mirror of
https://github.com/owncloud/ocis.git
synced 2026-10-23 23:29:02 +03:00
fix(web): [OCISDEV-1518] show correct message when Safe access is denied for lacking permission (#13111)
* fix(web): [OCISDEV-1518] show correct message when Safe access is denied for lacking permission * chore: changelog
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
Bugfix: Show correct message when Safe access is denied for lacking permission
|
||||
|
||||
A user without permission to use the Safe who opened the Safe URL directly was
|
||||
shown "Not logged in" with a "Log in again" button, even though they were
|
||||
still logged in. The page now shows "Access denied" with a "Continue" button
|
||||
that returns to the app for this case.
|
||||
|
||||
https://github.com/owncloud/ocis/pull/13111
|
||||
@@ -51,6 +51,7 @@ export default defineComponent({
|
||||
const { currentTheme } = storeToRefs(themeStore)
|
||||
const configStore = useConfigStore()
|
||||
const redirectUrlQuery = useRouteQuery('redirectUrl')
|
||||
const reasonQuery = useRouteQuery('reason')
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
@@ -58,18 +59,34 @@ export default defineComponent({
|
||||
const footerSlogan = computed(() => currentTheme.value.common.slogan)
|
||||
const logoImg = computed(() => currentTheme.value.logo.login)
|
||||
|
||||
// "forbidden" means the user is still logged in but lacks permission for this area
|
||||
// (e.g. no Vault access) — distinct from an actual session/logout problem.
|
||||
const isForbidden = computed(() => queryItemAsString(unref(reasonQuery)) === 'forbidden')
|
||||
|
||||
const cardTitle = computed(() => {
|
||||
return $gettext('Not logged in')
|
||||
return unref(isForbidden) ? $gettext('Access denied') : $gettext('Not logged in')
|
||||
})
|
||||
const cardHint = computed(() => {
|
||||
return $gettext(
|
||||
'This could be because of a routine safety log out, or because your account is either inactive or not yet authorized for use. Please try logging in after a while or seek help from your Administrator.'
|
||||
)
|
||||
return unref(isForbidden)
|
||||
? $gettext('You do not have permission to access this area. You are still logged in.')
|
||||
: $gettext(
|
||||
'This could be because of a routine safety log out, or because your account is either inactive or not yet authorized for use. Please try logging in after a while or seek help from your Administrator.'
|
||||
)
|
||||
})
|
||||
const navigateToLoginText = computed(() => {
|
||||
return $gettext('Log in again')
|
||||
return unref(isForbidden) ? $gettext('Continue') : $gettext('Log in again')
|
||||
})
|
||||
const logoutButtonsAttrs = computed(() => {
|
||||
// Still logged in — send back into the app, not through the login flow.
|
||||
if (unref(isForbidden)) {
|
||||
// Same fallback the login redirect uses: don't target a specific extension's
|
||||
// route, let the app resolve its own default/home view.
|
||||
return {
|
||||
type: 'router-link',
|
||||
to: '/'
|
||||
}
|
||||
}
|
||||
|
||||
const redirectUrl = queryItemAsString(unref(redirectUrlQuery))
|
||||
if (configStore.options.loginUrl) {
|
||||
const configLoginURL = new URL(encodeURI(configStore.options.loginUrl))
|
||||
|
||||
@@ -152,7 +152,7 @@ export class AuthService implements AuthServiceInterface {
|
||||
if (to.params.scope === 'vault') {
|
||||
// Permissions known: send an unentitled user to accessDenied instead of the IdP.
|
||||
if (this.authStore.userContextReady && !this.ability.can('read-all', 'Vault')) {
|
||||
return { name: 'accessDenied' }
|
||||
return { name: 'accessDenied', query: { reason: 'forbidden' } }
|
||||
}
|
||||
|
||||
// Capabilities only — the full context loads vault spaces, which 401 without MFA and crash.
|
||||
@@ -173,7 +173,7 @@ export class AuthService implements AuthServiceInterface {
|
||||
return { name: 'accessDenied' }
|
||||
}
|
||||
if (!this.ability.can('read-all', 'Vault')) {
|
||||
return { name: 'accessDenied' }
|
||||
return { name: 'accessDenied', query: { reason: 'forbidden' } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import accessDenied from '../../../src/pages/accessDenied.vue'
|
||||
import { defaultComponentMocks, defaultPlugins, mount } from '@ownclouders/web-test-helpers'
|
||||
import { mock } from 'vitest-mock-extended'
|
||||
import { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
const selectors = {
|
||||
logInAgainButton: '#exitAnchor'
|
||||
logInAgainButton: '#exitAnchor',
|
||||
cardTitle: '.oc-login-card-title'
|
||||
}
|
||||
|
||||
describe('access denied page', () => {
|
||||
@@ -23,11 +26,26 @@ describe('access denied page', () => {
|
||||
expect(loginAgainUrl.toString()).toEqual(loginUrl)
|
||||
})
|
||||
})
|
||||
describe('reason=forbidden (still logged in, lacking permission)', () => {
|
||||
it('does not claim the user was logged out, and sends them back into the app', () => {
|
||||
const { wrapper } = getWrapper({ reason: 'forbidden' })
|
||||
|
||||
expect(wrapper.find(selectors.cardTitle).text()).not.toContain('logged')
|
||||
expect(wrapper.html()).not.toContain('log out')
|
||||
|
||||
const goBackButton = wrapper.find(selectors.logInAgainButton)
|
||||
const destination = Object.values(goBackButton.attributes()).join(' ') + goBackButton.html()
|
||||
expect(destination.toLowerCase()).not.toContain('login')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function getWrapper({ loginUrl = '' } = {}) {
|
||||
function getWrapper({ loginUrl = '', reason = '' } = {}) {
|
||||
const currentRoute = mock<RouteLocationNormalizedLoaded>({
|
||||
query: { ...(reason && { reason }) }
|
||||
})
|
||||
const mocks = {
|
||||
...defaultComponentMocks()
|
||||
...defaultComponentMocks({ currentRoute })
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -555,7 +555,7 @@ describe('AuthService', () => {
|
||||
|
||||
// initializeContext returns the redirect target; the guard (setupAuthGuard) returns it
|
||||
// so vue-router cancels the navigation to the vault route.
|
||||
expect(result).toEqual({ name: 'accessDenied' })
|
||||
expect(result).toEqual({ name: 'accessDenied', query: { reason: 'forbidden' } })
|
||||
expect(mockSignInRedirect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -600,7 +600,7 @@ describe('AuthService', () => {
|
||||
const result = await authService.initializeContext(vaultRoute)
|
||||
|
||||
expect(mockLoadUserAbilities).toHaveBeenCalled()
|
||||
expect(result).toEqual({ name: 'accessDenied' })
|
||||
expect(result).toEqual({ name: 'accessDenied', query: { reason: 'forbidden' } })
|
||||
expect(mockSignInRedirect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -619,6 +619,22 @@ describe('AuthService', () => {
|
||||
expect(mockSignInRedirect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not report a load failure as a confirmed permission denial', async () => {
|
||||
const { authService, mockLoadUserAbilities } = setupVaultAuthService({
|
||||
canAccessVault: true,
|
||||
userContextReady: false,
|
||||
getUser: () => Promise.resolve(mock<User>({ profile: { acr: 'regular' }, expired: false }))
|
||||
})
|
||||
mockLoadUserAbilities.mockRejectedValueOnce(new Error('network error'))
|
||||
|
||||
const result = await authService.initializeContext(vaultRoute)
|
||||
|
||||
expect(mockLoadUserAbilities).toHaveBeenCalled()
|
||||
// reason: 'forbidden' tells accessDenied.vue the ability check ran and failed;
|
||||
// here it never ran, so claiming that is false regardless of the (correct) denial.
|
||||
expect((result as { query?: { reason?: string } })?.query?.reason).not.toBe('forbidden')
|
||||
})
|
||||
|
||||
it('on a cold load with a valid MFA session, initializes the user context instead of bouncing to login', async () => {
|
||||
const { authService, mockSignInRedirect, mockUpdateContext } = setupVaultAuthService({
|
||||
canAccessVault: true,
|
||||
|
||||
Reference in New Issue
Block a user