Skip to content

Commit 5914f3a

Browse files
authored
Merge commit from fork
[Don't merge as is! v6.24.5] Move from cookie-based to token-based authentication
2 parents 454d978 + 0ed0f3f commit 5914f3a

File tree

22 files changed

+144
-52
lines changed

22 files changed

+144
-52
lines changed

analyzer/config/analyzer_version.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"version": {
33
"major" : "6",
44
"minor" : "24",
5-
"revision" : "4",
5+
"revision" : "5",
66
"rc" : ""
77
}
88
}

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def run(self):
142142

143143
setuptools.setup(
144144
name="codechecker",
145-
version="6.24.4",
145+
version="6.24.5",
146146
author='CodeChecker Team (Ericsson)',
147147
author_email='[email protected]',
148148
description="CodeChecker is an analyzer tooling, defect database and "

snap/snapcraft.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: codechecker
22
base: core18
3-
version: '6.24.4'
3+
version: '6.24.5'
44
summary: CodeChecker is an analyzer tooling, defect database and viewer extension
55
description: |
66
CodeChecker is an analyzer tooling, defect database and viewer extension.
@@ -33,7 +33,7 @@ parts:
3333
codechecker:
3434
plugin: python
3535
python-version: python3
36-
source: https://github.com/Ericsson/codechecker/archive/v6.24.4.tar.gz
36+
source: https://github.com/Ericsson/codechecker/archive/v6.24.5.tar.gz
3737
build-packages:
3838
- curl
3939
- gcc-multilib
Binary file not shown.
Binary file not shown.

web/api/js/codechecker-api-node/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codechecker-api",
3-
"version": "6.58.0",
3+
"version": "6.59.0",
44
"description": "Generated node.js compatible API stubs for CodeChecker server.",
55
"main": "lib",
66
"homepage": "https://github.com/Ericsson/codechecker",
Binary file not shown.

web/api/py/codechecker_api/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
with open('README.md', encoding='utf-8', errors="ignore") as f:
99
long_description = f.read()
1010

11-
api_version = '6.58.0'
11+
api_version = '6.59.0'
1212

1313
setup(
1414
name='codechecker_api',
Binary file not shown.

web/api/py/codechecker_api_shared/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
with open('README.md', encoding='utf-8', errors="ignore") as f:
99
long_description = f.read()
1010

11-
api_version = '6.58.0'
11+
api_version = '6.59.0'
1212

1313
setup(
1414
name='codechecker_api_shared',

web/client/codechecker_client/helpers/base.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from thrift.transport import THttpClient
1414
from thrift.protocol import TJSONProtocol
1515

16-
from codechecker_client.credential_manager import SESSION_COOKIE_NAME
1716
from codechecker_client.product import create_product_url
1817

1918
from codechecker_common.logger import get_logger
@@ -72,7 +71,7 @@ def _set_token(self, session_token):
7271
if not session_token:
7372
return
7473

75-
headers = {'Cookie': SESSION_COOKIE_NAME + '=' + session_token}
74+
headers = {'Authorization': 'Bearer ' + session_token}
7675
self.transport.setCustomHeaders(headers)
7776

7877
def _reset_token(self):

web/codechecker_web/shared/version.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313

1414
# The name of the cookie which contains the user's authentication session's
1515
# token.
16+
# DEPRECATED: Session-based authentication will be removed in a future version.
17+
# Use the Authorization header instead.
1618
SESSION_COOKIE_NAME = '__ccPrivilegedAccessToken'
1719

1820
# The newest supported minor version (value) for each supported major version
1921
# (key) in this particular build.
2022
SUPPORTED_VERSIONS = {
21-
6: 58
23+
6: 59
2224
}
2325

2426
# Used by the client to automatically identify the latest major and minor

web/config/web_version.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"version": {
33
"major" : "6",
44
"minor" : "24",
5-
"revision" : "4",
5+
"revision" : "5",
66
"rc" : ""
77
}
88
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
Clear legacy web sessions
3+
4+
Revision ID: f59dfe4623fa
5+
Revises: 00099e8bc212
6+
Create Date: 2025-01-06 22:42:09.589909
7+
"""
8+
9+
from logging import getLogger
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
14+
15+
16+
# Revision identifiers, used by Alembic.
17+
revision = 'f59dfe4623fa'
18+
down_revision = '00099e8bc212'
19+
branch_labels = None
20+
depends_on = None
21+
22+
23+
def upgrade():
24+
op.execute("DELETE FROM auth_sessions WHERE can_expire")
25+
26+
def downgrade():
27+
pass

web/server/codechecker_server/server.py

+72-19
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def send_thrift_exception(self, error_msg, iprot, oprot, otrans):
103103
self.end_headers()
104104
self.wfile.write(result)
105105

106-
def __check_session_cookie(self):
106+
def __check_session_header(self):
107107
"""
108108
Check the CodeChecker privileged access cookie in the request headers.
109109
@@ -115,9 +115,19 @@ def __check_session_cookie(self):
115115
return None
116116

117117
session = None
118+
# Check if the user has presented a bearer token for authentication.
119+
token = self.headers.get("Authorization")
120+
if token and token.startswith("Bearer "):
121+
token = token.split("Bearer ", 1)[1]
122+
session = self.server.manager.get_session(token)
123+
118124
# Check if the user has presented a privileged access cookie.
125+
# This method is used by older command-line clients, and since cookies
126+
# with an expiration date were purged when updating, the web interface
127+
# should no longer have a valid access token as a cookie.
128+
# DEPRECATED: Will be removed in a future version.
119129
cookies = self.headers.get("Cookie")
120-
if cookies:
130+
if not session and cookies:
121131
split = cookies.split("; ")
122132
for cookie in split:
123133
values = cookie.split("=")
@@ -126,13 +136,13 @@ def __check_session_cookie(self):
126136
session = self.server.manager.get_session(values[1])
127137

128138
if session and session.is_alive:
129-
# If a valid session token was found and it can still be used,
139+
# If a valid bearer token was found and it can still be used,
130140
# mark that the user's last access to the server was the
131141
# request that resulted in the execution of this function.
132142
session.revalidate()
133143
return session
134144
else:
135-
# If the user's access cookie is no longer usable (invalid),
145+
# If the user's token is no longer usable (invalid),
136146
# present an error.
137147
client_host, client_port, is_ipv6 = \
138148
RequestHandler._get_client_host_port(self.client_address)
@@ -168,22 +178,46 @@ def __handle_liveness(self):
168178
self.wfile.write(b'CODECHECKER_SERVER_IS_LIVE')
169179

170180
def end_headers(self):
171-
# Sending the authentication cookie
172-
# in every response if any.
173-
# This will update the the session cookie
174-
# on the clients to the newest.
175-
if self.auth_session:
176-
token = self.auth_session.token
177-
if token:
178-
self.send_header(
179-
"Set-Cookie",
180-
f"{session_manager.SESSION_COOKIE_NAME}={token}; Path=/")
181+
"""
182+
Headers in this section are based on the OWASP Secure Headers Project.
183+
https://owasp.org/www-project-secure-headers/
184+
They are adapted to not allow any cross-site requests.
185+
"""
186+
if self.command in ['GET', 'HEAD', 'POST']:
187+
self.send_header('X-Frame-Options', 'DENY')
188+
self.send_header('X-XSS-Protection', '1; mode=block')
189+
self.send_header('X-Content-Type-Options', 'nosniff')
190+
self.send_header('Content-Security-Policy',
191+
'default-src \'self\'; ' +
192+
'style-src \'unsafe-inline\' \'self\'; ' +
193+
'script-src \'unsafe-inline\' \'self\'; ' +
194+
'form-action \'self\'; ' +
195+
'frame-ancestors \'none\'; ' +
196+
'upgrade-insecure-requests; ' +
197+
'block-all-mixed-content')
198+
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
199+
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
200+
self.send_header('Cross-Origin-Resource-Policy', 'same-origin')
201+
self.send_header('Referrer-Policy', 'no-referrer')
181202

203+
if self.auth_session:
182204
# Set the current user name in the header.
183205
user_name = self.auth_session.user
184206
if user_name:
185207
self.send_header("X-User", user_name)
186208

209+
# If the user has a leftover session cookie, try removing it.
210+
# Command-line clients ignore overwriting the cookie, but web clients
211+
# will remove the cookie from the browser.
212+
# DEPRECATED: Will be removed in a future version.
213+
elif self.headers.get('Cookie'):
214+
self.send_header('Set-Cookie',
215+
session_manager.SESSION_COOKIE_NAME + '=; ' +
216+
'Path=/; ' +
217+
'Max-Age=0; ' +
218+
'HttpOnly; ' +
219+
'SameSite=Strict')
220+
187221
SimpleHTTPRequestHandler.end_headers(self)
188222

189223
@staticmethod
@@ -199,6 +233,23 @@ def _get_client_host_port(address):
199233

200234
raise IndexError("Invalid address tuple given.")
201235

236+
def do_OPTIONS(self): # pylint: disable=C0103
237+
"""
238+
Handle OPTIONS requests.
239+
240+
Always returns 403 to indicate that no cross-site requests are allowed.
241+
No CORS heeaders are allowed next to the 403 response.
242+
"""
243+
client_host, client_port, is_ipv6 = \
244+
RequestHandler._get_client_host_port(self.client_address)
245+
246+
LOG.debug("%s:%s -- [Anonymous] OPTIONS %s",
247+
client_host if not is_ipv6 else '[' + client_host + ']',
248+
client_port, self.path)
249+
250+
self.send_response(403)
251+
self.end_headers()
252+
202253
def do_GET(self):
203254
""" Handles the SPA browser access (GET requests).
204255
@@ -212,12 +263,14 @@ def do_GET(self):
212263
"""
213264
client_host, client_port, is_ipv6 = \
214265
RequestHandler._get_client_host_port(self.client_address)
215-
self.auth_session = self.__check_session_cookie()
216266

217-
username = self.auth_session.user if self.auth_session else 'Anonymous'
218-
LOG.debug("%s:%s -- [%s] GET %s",
267+
# GET requests are served from www_root.
268+
self.directory = self.server.www_root
269+
270+
# Bearer tokens are not sent alongside GET requests.
271+
LOG.debug("%s:%s -- [Anonymous] GET %s",
219272
client_host if not is_ipv6 else '[' + client_host + ']',
220-
client_port, username, self.path)
273+
client_port, self.path)
221274

222275
if self.path == '/':
223276
self.path = 'index.html'
@@ -310,7 +363,7 @@ def do_POST(self):
310363

311364
client_host, client_port, is_ipv6 = \
312365
RequestHandler._get_client_host_port(self.client_address)
313-
self.auth_session = self.__check_session_cookie()
366+
self.auth_session = self.__check_session_header()
314367
auth_user = \
315368
self.auth_session.user if self.auth_session else "Anonymous"
316369
host_info = client_host if not is_ipv6 else '[' + client_host + ']'

web/server/codechecker_server/session_manager.py

+9
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ def is_alive(self):
118118
return (datetime.now() - self.last_access).total_seconds() <= \
119119
self.session_lifetime
120120

121+
@property
122+
def can_expire(self):
123+
"""
124+
Returns if the session can expire.
125+
Expiring sessions are created through the web interface, non-expiring
126+
sessions are created through the command-line client.
127+
"""
128+
return self.__can_expire
129+
121130
def revalidate(self):
122131
"""
123132
A session is only revalidated if it has yet to exceed its

web/server/vue-cli/package-lock.json

+6-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/server/vue-cli/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
},
2828
"dependencies": {
2929
"@mdi/font": "^6.5.95",
30-
"codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.58.0.tgz",
30+
"codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz",
3131
"chart.js": "^2.9.4",
3232
"chartjs-plugin-datalabels": "^0.7.0",
3333
"codemirror": "^5.65.0",

web/server/vue-cli/src/services/api/_base.service.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import router from "@/router";
1010
import store from "@/store";
1111
import { ADD_ERROR, PURGE_AUTH } from "@/store/mutations.type";
12+
import authService from "./auth.service";
1213

1314
// Host should be set explicitly to `hostname` because thrift will use
1415
// the value of `window.location.host` which will contain port number by
@@ -51,7 +52,10 @@ class BaseService {
5152
const xreq = getXmlHttpRequestObject();
5253

5354
xreq.addEventListener("readystatechange", function () {
54-
if (this.readyState === 4) {
55+
if (this.readyState === 1) { // connection opened, headers not sent yet
56+
xreq.setRequestHeader("Authorization",
57+
"Bearer " + authService.getToken());
58+
} else if (this.readyState === 4) { // request finished
5559
if (this.status === 504) {
5660
store.commit(ADD_ERROR,
5761
`Error ${this.status}: ${this.statusText}`);
@@ -94,14 +98,14 @@ const handleThriftError = function (cb, onError) {
9498
router.push({
9599
name: "login",
96100
query: { "return_to": router.currentRoute.fullPath }
97-
}).catch(() => {});
101+
}).catch(() => { });
98102

99103
if (onError) onError(err);
100104
return;
101105
} else if (msg.search(/The product .* does not exist!/) > -1) {
102106
return router.replace({
103107
name: "404"
104-
}).catch(() => {});
108+
}).catch(() => { });
105109
}
106110
}
107111

0 commit comments

Comments
 (0)