mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-26 22:04:56 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973dd9f06d | ||
|
|
8bc4e5ee94 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "paperless-ngx"
|
||||
version = "3.0.3"
|
||||
version = "3.0.2"
|
||||
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paperless-ngx-ui",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"ng": "ng",
|
||||
|
||||
@@ -108,6 +108,18 @@ describe('PermissionsService', () => {
|
||||
actionKey: 'View', // PermissionAction.View
|
||||
typeKey: 'Document', // PermissionType.Document
|
||||
})
|
||||
expect(
|
||||
permissionsService.getPermissionKeys('view_global_statistics')
|
||||
).toEqual({
|
||||
actionKey: 'View', // PermissionAction.View
|
||||
typeKey: 'GlobalStatistics', // PermissionType.GlobalStatistics
|
||||
})
|
||||
expect(
|
||||
permissionsService.getPermissionKeys('view_system_monitoring')
|
||||
).toEqual({
|
||||
actionKey: 'View', // PermissionAction.View
|
||||
typeKey: 'SystemMonitoring', // PermissionType.SystemMonitoring
|
||||
})
|
||||
})
|
||||
|
||||
it('correctly checks explicit global permissions', () => {
|
||||
|
||||
@@ -110,19 +110,16 @@ export class PermissionsService {
|
||||
actionKey: string
|
||||
typeKey: string
|
||||
} {
|
||||
const matches = permissionStr.match(/(.+)_/)
|
||||
let typeKey
|
||||
let actionKey
|
||||
if (matches?.length > 0) {
|
||||
const action = matches[1]
|
||||
const actionIndex = Object.values(PermissionAction).indexOf(
|
||||
action as PermissionAction
|
||||
)
|
||||
if (actionIndex > -1) {
|
||||
actionKey = Object.keys(PermissionAction)[actionIndex]
|
||||
}
|
||||
const actionIndex = Object.values(PermissionAction).findIndex((action) =>
|
||||
permissionStr.startsWith(`${action}_`)
|
||||
)
|
||||
if (actionIndex > -1) {
|
||||
const action = Object.values(PermissionAction)[actionIndex]
|
||||
actionKey = Object.keys(PermissionAction)[actionIndex]
|
||||
const typeIndex = Object.values(PermissionType).indexOf(
|
||||
permissionStr.replace(action, '%s') as PermissionType
|
||||
permissionStr.replace(`${action}_`, '%s_') as PermissionType
|
||||
)
|
||||
if (typeIndex > -1) {
|
||||
typeKey = Object.keys(PermissionType)[typeIndex]
|
||||
|
||||
@@ -8,7 +8,7 @@ export const environment = {
|
||||
apiVersion: '10', // match src/paperless/settings.py
|
||||
appTitle: DEFAULT_APP_TITLE,
|
||||
tag: 'prod',
|
||||
version: '3.0.3',
|
||||
version: '3.0.2',
|
||||
webSocketHost: window.location.host,
|
||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||
|
||||
@@ -172,3 +172,39 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
||||
self.assertIsNotNone(
|
||||
response.data["settings"]["outlook_oauth_url"],
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
OAUTH_CALLBACK_BASE_URL="http://localhost:8000",
|
||||
GMAIL_OAUTH_CLIENT_ID="abc123",
|
||||
GMAIL_OAUTH_CLIENT_SECRET="def456",
|
||||
GMAIL_OAUTH_ENABLED=True,
|
||||
OUTLOOK_OAUTH_CLIENT_ID="ghi789",
|
||||
OUTLOOK_OAUTH_CLIENT_SECRET="jkl012",
|
||||
OUTLOOK_OAUTH_ENABLED=True,
|
||||
)
|
||||
def test_settings_oauth_state_reused_across_calls(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- A mail account oauth flow has been started, storing a state in the session
|
||||
WHEN:
|
||||
- The ui_settings endpoint is called again before the flow completes
|
||||
THEN:
|
||||
- The same oauth state is reused, not replaced with a new one
|
||||
"""
|
||||
response = self.client.get(self.ENDPOINT, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
first_state = self.client.session["oauth_state"]
|
||||
first_gmail_url = response.data["settings"]["gmail_oauth_url"]
|
||||
first_outlook_url = response.data["settings"]["outlook_oauth_url"]
|
||||
|
||||
response = self.client.get(self.ENDPOINT, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(self.client.session["oauth_state"], first_state)
|
||||
self.assertEqual(
|
||||
response.data["settings"]["gmail_oauth_url"],
|
||||
first_gmail_url,
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data["settings"]["outlook_oauth_url"],
|
||||
first_outlook_url,
|
||||
)
|
||||
|
||||
@@ -4002,15 +4002,19 @@ class UiSettingsView(GenericAPIView[Any]):
|
||||
ui_settings["auditlog_enabled"] = settings.AUDIT_LOG_ENABLED
|
||||
|
||||
if settings.GMAIL_OAUTH_ENABLED or settings.OUTLOOK_OAUTH_ENABLED:
|
||||
manager = PaperlessMailOAuth2Manager()
|
||||
# Reuse an in-flight state instead of minting a new one on every
|
||||
# settings fetch, or a concurrent request can invalidate a login
|
||||
# that's still in progress.
|
||||
manager = PaperlessMailOAuth2Manager(
|
||||
state=request.session.get("oauth_state"),
|
||||
)
|
||||
request.session["oauth_state"] = manager.state
|
||||
if settings.GMAIL_OAUTH_ENABLED:
|
||||
ui_settings["gmail_oauth_url"] = manager.get_gmail_authorization_url()
|
||||
request.session["oauth_state"] = manager.state
|
||||
if settings.OUTLOOK_OAUTH_ENABLED:
|
||||
ui_settings["outlook_oauth_url"] = (
|
||||
manager.get_outlook_authorization_url()
|
||||
)
|
||||
request.session["oauth_state"] = manager.state
|
||||
|
||||
ui_settings["email_enabled"] = settings.EMAIL_ENABLED
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Final
|
||||
|
||||
__version__: Final[tuple[int, int, int]] = (3, 0, 3)
|
||||
__version__: Final[tuple[int, int, int]] = (3, 0, 2)
|
||||
# Version string like X.Y.Z
|
||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
||||
# Version string like X.Y
|
||||
|
||||
@@ -138,6 +138,16 @@ class TestMailOAuth(
|
||||
MailAccount.objects.filter(imap_server="imap.gmail.com").exists(),
|
||||
)
|
||||
|
||||
# State is single-use and was cleared by the callback above, so a new
|
||||
# flow needs a new state stored in the session.
|
||||
session = self.client.session
|
||||
session.update(
|
||||
{
|
||||
"oauth_state": "test_state",
|
||||
},
|
||||
)
|
||||
session.save()
|
||||
|
||||
# Test Outlook OAuth callback
|
||||
response = self.client.get(
|
||||
"/api/oauth/callback/?code=test_code&state=test_state",
|
||||
@@ -180,6 +190,16 @@ class TestMailOAuth(
|
||||
MailAccount.objects.filter(imap_server="imap.gmail.com").exists(),
|
||||
)
|
||||
|
||||
# State is single-use and was cleared by the callback above, so a
|
||||
# new flow needs a new state stored in the session.
|
||||
session = self.client.session
|
||||
session.update(
|
||||
{
|
||||
"oauth_state": "test_state",
|
||||
},
|
||||
)
|
||||
session.save()
|
||||
|
||||
# Test Outlook OAuth callback
|
||||
response = self.client.get(
|
||||
"/api/oauth/callback/?code=test_code&state=test_state",
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.http import HttpResponseRedirect
|
||||
@@ -256,9 +257,14 @@ class OauthCallbackView(GenericAPIView[Any]):
|
||||
)
|
||||
return HttpResponseBadRequest("Invalid request, see logs for more detail")
|
||||
|
||||
oauth_manager = PaperlessMailOAuth2Manager(
|
||||
state=request.session.get("oauth_state"),
|
||||
)
|
||||
session_state = request.session.get("oauth_state")
|
||||
if not session_state and not settings.DEBUG:
|
||||
logger.error(
|
||||
"Invalid oauth callback request: no state in session",
|
||||
)
|
||||
return HttpResponseBadRequest("Invalid request, see logs for more detail")
|
||||
|
||||
oauth_manager = PaperlessMailOAuth2Manager(state=session_state)
|
||||
|
||||
state = request.query_params.get("state", "")
|
||||
if not oauth_manager.validate_state(state):
|
||||
@@ -315,3 +321,5 @@ class OauthCallbackView(GenericAPIView[Any]):
|
||||
return HttpResponseRedirect(
|
||||
f"{oauth_manager.oauth_redirect_url}?oauth_success=0",
|
||||
)
|
||||
finally:
|
||||
request.session.pop("oauth_state", None)
|
||||
|
||||
Reference in New Issue
Block a user