Skip to content
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

Metadata Editor: redirect to login URL if accessing the editor as anonymous user #1006

Merged
merged 2 commits into from
Sep 26, 2024
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
20 changes: 14 additions & 6 deletions apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ const fakeUser = {

const gnBaseUrl = 'http://localhost:8080/geonetwork/srv/eng/'

describe('dashboard', () => {
describe('dashboard (authenticated)', () => {
beforeEach(() => {
cy.login('admin', 'admin', false)
})

let pageOne
describe('avatar', () => {
describe('display avatar for user without gravatar hash', () => {
it('should display placeholder url', () => {
cy.login('admin', 'admin', false)
cy.visit(`${gnBaseUrl}admin.console#/organization`)
cy.get('#gn-btn-user-add').click()
cy.get('#username').type(fakeUser.username)
Expand All @@ -35,7 +38,6 @@ describe('dashboard', () => {
.should('eq', 'https://www.gravatar.com/avatar/?d=mp')
})
it('should display monsterid', () => {
cy.login('admin', 'admin', false)
cy.visit(`${gnBaseUrl}admin.console#/settings`)
cy.get('[id="system/users/identicon"]').type(
'{selectAll}gravatar:monsterid'
Expand Down Expand Up @@ -160,7 +162,6 @@ describe('dashboard', () => {
})
describe('columns', () => {
beforeEach(() => {
cy.login('admin', 'admin', false)
cy.visit('/catalog/search')
})
it('should display the right info for unpublished records', () => {
Expand Down Expand Up @@ -216,7 +217,6 @@ describe('dashboard', () => {

describe('navigation', () => {
beforeEach(() => {
cy.login('admin', 'admin', false)
cy.visit('/catalog/search')
})
describe('all records', () => {
Expand Down Expand Up @@ -277,7 +277,6 @@ describe('dashboard', () => {
}
describe('allRecords search input', () => {
beforeEach(() => {
cy.login('admin', 'admin', false)
cy.visit('/catalog/search')
})
it('should filter the dashboard based on the search input', () => {
Expand Down Expand Up @@ -315,3 +314,12 @@ describe('dashboard', () => {
})
})
})

describe('when the user is not logged in', () => {
beforeEach(() => {
cy.visit('/catalog/search')
})
it('redirects to the login page', () => {
cy.url().should('include', '/catalog.signin?redirect=')
})
})
10 changes: 9 additions & 1 deletion apps/metadata-editor/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import { provideAnimations } from '@angular/platform-browser/animations'
import { extModules } from './build-specifics'
import { DashboardPageComponent } from './dashboard/dashboard-page.component'
import { EditorRouterService } from './router.service'
import { provideGn4, provideRepositoryUrl } from '@geonetwork-ui/api/repository'
import {
LOGIN_URL,
provideGn4,
provideRepositoryUrl,
} from '@geonetwork-ui/api/repository'
import { FeatureEditorModule } from '@geonetwork-ui/feature/editor'

@NgModule({
Expand Down Expand Up @@ -62,6 +66,10 @@ import { FeatureEditorModule } from '@geonetwork-ui/feature/editor'
importProvidersFrom(EffectsModule.forRoot()),
provideGn4(),
provideAnimations(),
{
provide: LOGIN_URL,
useFactory: () => getGlobalConfig().LOGIN_URL,
},
],
bootstrap: [AppComponent],
})
Expand Down
159 changes: 84 additions & 75 deletions apps/metadata-editor/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Route } from '@angular/router'
import { DashboardPageComponent } from './dashboard/dashboard-page.component'
import { SignInPageComponent } from './sign-in/sign-in-page.component'
import { EditPageComponent } from './edit/edit-page.component'
import { EditRecordResolver } from './edit-record.resolver'
import { MyDraftComponent } from './records/my-draft/my-draft.component'
Expand All @@ -10,100 +9,110 @@ import { NewRecordResolver } from './new-record.resolver'
import { DuplicateRecordResolver } from './duplicate-record.resolver'
import { AllRecordsComponent } from './records/all-records/all-records.component'
import { MyRecordsStateWrapperComponent } from './records/my-records/my-records-state-wrapper.component'
import { AuthGuardService } from './auth-guard.service'

export const appRoutes: Route[] = [
{ path: '', redirectTo: 'catalog/search', pathMatch: 'prefix' },
{
path: 'catalog',
component: DashboardPageComponent,
outlet: 'primary',
path: '',
canActivate: [AuthGuardService],
children: [
Copy link
Collaborator Author

@jahow jahow Sep 26, 2024

Choose a reason for hiding this comment

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

Adding a root route ( 🤔 ) makes it easier to apply the same guard to all routes

{
path: '',
redirectTo: 'search',
redirectTo: 'catalog/search',
pathMatch: 'prefix',
},
{
path: 'discussion',
component: AllRecordsComponent,
pathMatch: 'prefix',
path: 'catalog',
component: DashboardPageComponent,
outlet: 'primary',
children: [
{
path: '',
redirectTo: 'search',
pathMatch: 'prefix',
},
{
path: 'discussion',
component: AllRecordsComponent,
pathMatch: 'prefix',
},
{
path: 'calendar',
component: AllRecordsComponent,
pathMatch: 'prefix',
},
{
path: 'contacts',
component: AllRecordsComponent,
pathMatch: 'prefix',
},
{
path: 'thesaurus',
component: AllRecordsComponent,
pathMatch: 'prefix',
},
{
path: 'search',
title: 'Search Records',
component: AllRecordsComponent,
pathMatch: 'prefix',
},
],
},
{
path: 'calendar',
component: AllRecordsComponent,
pathMatch: 'prefix',
path: 'my-space',
component: DashboardPageComponent,
outlet: 'primary',
title: 'My space',
children: [
{
path: 'my-records',
title: 'My Records',
component: MyRecordsStateWrapperComponent,
pathMatch: 'prefix',
},
{
path: 'my-draft',
title: 'My Draft',
component: MyDraftComponent,
pathMatch: 'prefix',
},
{
path: 'templates',
title: 'Templates',
component: TemplatesComponent,
pathMatch: 'prefix',
},
],
},
{
path: 'contacts',
component: AllRecordsComponent,
pathMatch: 'prefix',
path: 'users',
component: DashboardPageComponent,
outlet: 'primary',
title: 'Users',
children: [
{
path: 'my-org',
component: MyOrgUsersComponent,
pathMatch: 'prefix',
},
],
},
{
path: 'thesaurus',
component: AllRecordsComponent,
pathMatch: 'prefix',
path: 'create',
component: EditPageComponent,
resolve: { record: NewRecordResolver },
},
{
path: 'search',
title: 'Search Records',
component: AllRecordsComponent,
pathMatch: 'prefix',
path: 'duplicate/:uuid',
component: EditPageComponent,
resolve: { record: DuplicateRecordResolver },
},
],
},
{
path: 'my-space',
component: DashboardPageComponent,
outlet: 'primary',
title: 'My space',
children: [
{
path: 'my-records',
title: 'My Records',
component: MyRecordsStateWrapperComponent,
pathMatch: 'prefix',
},
{
path: 'my-draft',
title: 'My Draft',
component: MyDraftComponent,
pathMatch: 'prefix',
},
{
path: 'templates',
title: 'Templates',
component: TemplatesComponent,
pathMatch: 'prefix',
},
],
},
{
path: 'users',
component: DashboardPageComponent,
outlet: 'primary',
title: 'Users',
children: [
{
path: 'my-org',
component: MyOrgUsersComponent,
pathMatch: 'prefix',
path: 'edit/:uuid',
component: EditPageComponent,
resolve: { record: EditRecordResolver },
},
],
},
{ path: 'sign-in', component: SignInPageComponent },
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this route was removed as it was obviously not needed anyway

{
path: 'create',
component: EditPageComponent,
resolve: { record: NewRecordResolver },
},
{
path: 'duplicate/:uuid',
component: EditPageComponent,
resolve: { record: DuplicateRecordResolver },
},
{
path: 'edit/:uuid',
component: EditPageComponent,
resolve: { record: EditRecordResolver },
},
]
44 changes: 44 additions & 0 deletions apps/metadata-editor/src/app/auth-guard.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AuthGuardService } from './auth-guard.service'
import { MockBuilder, MockProvider } from 'ng-mocks'
import { AuthService } from '@geonetwork-ui/api/repository'
import { TestBed } from '@angular/core/testing'
import { of } from 'rxjs'
import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface'

Object.defineProperty(window, 'location', {
value: new URL('http://localhost'),
})

describe('AuthGuardService', () => {
let service: AuthGuardService
beforeEach(() => {
return MockBuilder(AuthGuardService)
})

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
MockProvider(AuthService, {
loginUrl: 'http://login.com/bla?',
}),
MockProvider(PlatformServiceInterface, {
isAnonymous: () => of(true),
}),
],
})
window.location.href = 'http://original.path'
service = TestBed.inject(AuthGuardService)
})

it('returns true if the user is logged in', async () => {
jest
.spyOn(TestBed.inject(PlatformServiceInterface), 'isAnonymous')
.mockReturnValue(of(false))
expect(await service.canActivate()).toBe(true)
expect(window.location.href).toBe('http://original.path/')
})
it('redirects the user to the login page if the user is not logged in', async () => {
expect(await service.canActivate()).toBe(false)
expect(window.location.href).toBe('http://login.com/bla?')
})
})
22 changes: 22 additions & 0 deletions apps/metadata-editor/src/app/auth-guard.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable } from '@angular/core'
import { AuthService } from '@geonetwork-ui/api/repository'
import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface'
import { firstValueFrom } from 'rxjs'

@Injectable({ providedIn: 'root' })
export class AuthGuardService {
constructor(
private platformService: PlatformServiceInterface,
private authService: AuthService
) {}

// this will redirect the user to the authentication form if required
async canActivate(): Promise<boolean> {
const notLoggedIn = await firstValueFrom(this.platformService.isAnonymous())
if (notLoggedIn) {
window.location.href = this.authService.loginUrl
return false
}
return true
}
}
Empty file.

This file was deleted.

This file was deleted.

This file was deleted.

11 changes: 8 additions & 3 deletions docs/guide/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
outline: deep
---

# FAQ
# Frequently Asked Questions

## Chapter 1
[[toc]]

## Chapter 2
### _I have deployed Application Name alongside GeoNetwork, but somehow all the HTTP requests going to GeoNetwork end up failing with a 403 error, why?_

There are several possible reasons for this:

- The attempted requests necessitate authentication (e.g. creating a record) but the session of the current user has expired; in this case, the user should log in again.
- The XSRF protection mechanism is not working correctly; this can be complicated to set up, please refer to [this part of the documentation](./deploy.md#authentication) to know more.
Loading