Skip to content
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
17 changes: 9 additions & 8 deletions api/v6/authentication.thrift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ namespace py Authentication_v6
namespace js codeCheckerAuthentication_v6

struct HandshakeInformation {
1: bool requiresAuthentication, // true if the server has a privileged zone --- the state of having a valid access is not considered here
2: bool sessionStillActive // whether the session in which the HandshakeInformation is returned is a valid one
1: bool requiresAuthentication, // True if the server has a privileged zone.
2: bool sessionStillActive // Whether the session in which the HandshakeInformation is returned is a live one
}

struct AuthorisationList {
Expand All @@ -36,24 +36,25 @@ service codeCheckerAuthentication {
throws (1: shared.RequestFailed requestError),

// ============= Authentication and session handling =============
// get basic authentication information from the server
// Get basic authentication information from the server.
HandshakeInformation getAuthParameters(),


// retrieves a list of accepted authentication methods from the server
// Retrieves a list of accepted authentication methods from the server.
list<string> getAcceptedAuthMethods(),

// handles creating a session token for the user
// Handles creating a session token for the user.
string performLogin(1: string authMethod,
2: string authString)
throws (1: shared.RequestFailed requestError),

// performs logout action for the user (must be called from the corresponding valid session)
// Performs logout action for the user. Must be called from the
// corresponding valid session which is to be destroyed.
bool destroySession()
throws (1: shared.RequestFailed requestError),

// returns currently logged in user within the active session
// returns empty string if the session is not active
// Returns the currently logged in user within the active session, or empty
// string if no authenticated session is active.
string getLoggedInUser()
throws (1: shared.RequestFailed requestError),

Expand Down
2 changes: 1 addition & 1 deletion libcodechecker/libclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def handle_auth(protocol, host, port, username, login=False):
pwd = saved_auth.split(":")[1]
else:
LOG.info("Logging in using credentials from command line...")
pwd = getpass.getpass("Please provide password for user '{0}'"
pwd = getpass.getpass("Please provide password for user '{0}': "
.format(username))

LOG.debug("Trying to login as {0} to {1}:{2}"
Expand Down
20 changes: 18 additions & 2 deletions libcodechecker/server/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

# A list of top-level path elements under the webserver root
# which should not be considered as a product route.
NON_PRODUCT_ENDPOINTS = ['products.html',
'index.html',
NON_PRODUCT_ENDPOINTS = ['index.html',
'login.html',
'products.html',
'fonts',
'images',
'scripts',
Expand All @@ -31,6 +32,13 @@
]


# A list of top-level path elements under the webserver root which should
# be protected by authentication requirement when accessing the server.
PROTECTED_ENTRY_POINTS = ['', # Empty string in a request is 'index.html'.
'index.html',
'products.html']


def is_valid_product_endpoint(uripart):
"""
Returns whether or not the given URI part is to be considered a valid
Expand Down Expand Up @@ -122,3 +130,11 @@ def split_client_POST_request(path):
remainder = split_path[2]

return None, version_tag, remainder


def is_protected_GET_entrypoint(path):
"""
Returns if the given GET request's PATH enters the server through an
entry point which is considered protected by authentication requirements.
"""
return path in PROTECTED_ENTRY_POINTS
228 changes: 109 additions & 119 deletions libcodechecker/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,6 @@ def __check_auth_in_request(self):
success = self.server.manager.get_session(values[1],
True)

if success is None:
# Session cookie was invalid (or not found...)
# Attempt to see if the browser has sent us
# an authentication request.
authHeader = self.headers.getheader("Authorization")
if authHeader is not None and authHeader.startswith("Basic "):
authString = base64.decodestring(
self.headers.getheader("Authorization").
replace("Basic ", ""))

session = self.server.manager.create_or_get_session(
authString)
if session:
return session

# Else, access is still not granted.
if success is None:
LOG.debug(client_host + ":" + str(client_port) +
Expand All @@ -147,7 +132,7 @@ def end_headers(self):

def do_GET(self):
"""
Handles the webbrowser access (GET requests).
Handles the browser access (GET requests).
"""

auth_session = self.__check_auth_in_request()
Expand All @@ -157,118 +142,123 @@ def do_GET(self):
auth_session.user if auth_session else 'Anonymous',
self.path))

if self.server.manager.is_enabled and not auth_session:
realm = self.server.manager.get_realm()['realm']
error_body = self.server.manager.get_realm()['error']
if auth_session is not None:
self.auth_token = auth_session.token

product_endpoint, path = routing.split_client_GET_request(self.path)

if self.server.manager.is_enabled and not auth_session \
and routing.is_protected_GET_entrypoint(path):
# If necessary, prompt the user for authentication.
returnto = '#returnto=' + urllib.quote_plus(self.path.lstrip('/'))\
if self.path != '/' else ''

self.send_response(401) # 401 Unauthorised
self.send_header('WWW-Authenticate',
'Basic realm="{0}"'.format(realm))
self.send_header('Content-type', 'text/plain')
self.send_header('Content-length', str(len(error_body)))
self.send_response(307) # 307 Temporary Redirect
self.send_header('Location', '/login.html' + returnto)
self.send_header('Connection', 'close')
self.end_headers()
self.wfile.write(error_body)
self.wfile.write('')
return
else:
if auth_session is not None:
self.auth_token = auth_session.token
product_endpoint, path = \
routing.split_client_GET_request(self.path)

if product_endpoint is not None and product_endpoint != '':

product = self.server.get_product(product_endpoint)

if not product:
LOG.info("Product endpoint '{0}' does not exist."
.format(product_endpoint))
self.send_error(
404,
"The product {0} does not exist."
.format(product_endpoint))
return

if product:
# Try to reconnect in these cases.
# Do not try to reconnect if there is a schema mismatch.
reconnect_cases = [DBStatus.FAILED_TO_CONNECT,
DBStatus.MISSING,
DBStatus.SCHEMA_INIT_ERROR]
# If the product is not connected, try reconnecting...
if product.db_status in reconnect_cases:
LOG.error("Request's product '{0}' is not connected! "
"Attempting reconnect..."
.format(product_endpoint))
product.connect()
if product.db_status != DBStatus.OK:
# If the reconnection fails,
# redirect user to the products page.
self.send_response(307) # 307 Temporary Redirect
self.send_header("Location", '/products.html')
self.end_headers()
return

if path == '' and not self.path.endswith('/'):
# /prod must be routed to /prod/index.html first, so later
# queries for web resources are '/prod/style...' as
# opposed to '/style...', which would result in "style"
# being considered product name.
LOG.debug("Redirecting user from /{0} to /{0}/index.html"
if product_endpoint is not None and product_endpoint != '':
# Route the user if there is a product endpoint in the request.

product = self.server.get_product(product_endpoint)
if not product:
# Give an error if the user tries to access an invalid product.
LOG.info("Product endpoint '{0}' does not exist."
.format(product_endpoint))
self.send_error(
404,
"The product {0} does not exist."
.format(product_endpoint))
return

if product:
# Try to reconnect in these cases.
# Do not try to reconnect if there is a schema mismatch.
reconnect_cases = [DBStatus.FAILED_TO_CONNECT,
DBStatus.MISSING,
DBStatus.SCHEMA_INIT_ERROR]
# If the product is not connected, try reconnecting...
if product.db_status in reconnect_cases:
LOG.error("Request's product '{0}' is not connected! "
"Attempting reconnect..."
.format(product_endpoint))

# WARN: Browsers cache '308 Permanent Redirect' responses,
# in the event of debugging this, use Private Browsing!
self.send_response(308)
self.send_header("Location",
self.path.replace(product_endpoint,
product_endpoint + '/',
1))
self.end_headers()
return
else:
# Serves the main page and the resources:
# /prod/(index.html) -> /(index.html)
# /prod/styles/(...) -> /styles/(...)
LOG.debug("Product routing before " + self.path)
self.path = self.path.replace(
"{0}/".format(product_endpoint), "", 1)
LOG.debug("Product routing after: " + self.path)
product.connect()
if product.db_status != DBStatus.OK:
# If the reconnection fails,
# redirect user to the products page.
self.send_response(307) # 307 Temporary Redirect
self.send_header("Location", '/products.html')
self.end_headers()
return

if path == '' and not self.path.endswith('/'):
# /prod must be routed to /prod/index.html first, so later
# queries for web resources are '/prod/style...' as
# opposed to '/style...', which would result in 'style'
# being considered product name.
LOG.debug("Redirecting user from /{0} to /{0}/index.html"
.format(product_endpoint))

# WARN: Browsers cache '308 Permanent Redirect' responses,
# in the event of debugging this, use Private Browsing!
self.send_response(308)
self.send_header("Location",
self.path.replace(product_endpoint,
product_endpoint + '/',
1))
self.end_headers()
return

# In other cases when '/prod/' is already in the request,
# serve the main page and the resources, for example:
# /prod/(index.html) -> /(index.html)
# /prod/styles/(...) -> /styles/(...)
LOG.debug("Product routing before " + self.path)
self.path = self.path.replace(
"{0}/".format(product_endpoint), "", 1)
LOG.debug("Product routing after: " + self.path)
else:
# No product endpoint in the request.

if self.path in ['/', '/index.html']:
# In case the homepage is requested and only one product
# exists, try to skip the product list and redirect the user
# to the runs immediately.
only_product = self.server.get_only_product()
if only_product:
if only_product.db_status == DBStatus.OK:
LOG.debug("Redirecting '/' to ONLY product '/{0}'"
.format(only_product.endpoint))

self.send_response(307) # 307 Temporary Redirect
self.send_header("Location",
'/{0}'.format(only_product.endpoint))
self.end_headers()
return
else:
LOG.debug("ONLY product '/{0}' has database issues..."
.format(only_product.endpoint))

self.send_response(307) # 307 Temporary Redirect
self.send_header("Location", '/products.html')
self.end_headers()
return

# If multiple products exist, route homepage queries to
# serve the product list.
LOG.debug("Serving product list as homepage.")
self.path = '/products.html'
else:
# The path requested does not specify a product: it is most
# likely a resource file.
LOG.debug("Serving resource '{0}'".format(self.path))

if self.path in ['/', '/index.html']:
only_product = self.server.get_only_product()
if only_product:
prod_db_status = only_product.db_status
if prod_db_status == DBStatus.OK:
LOG.debug("Redirecting '/' to ONLY product '/{0}'"
.format(only_product.endpoint))

self.send_response(307) # 307 Temporary Redirect
msg = '/{0}'.format(only_product.endpoint)
self.send_header("Location", msg)
self.end_headers()
return
else:
LOG.debug("Redirecting '/' to ONLY product '/{0}'"
.format(only_product.endpoint))

self.send_response(307) # 307 Temporary Redirect
self.send_header("Location", '/products.html')
self.end_headers()
return

# Route homepage queries to serving the product list.
LOG.debug("Serving product list as homepage.")
self.path = '/products.html'
else:
# The path requested does not specify a product: it is most
# likely a resource file.
LOG.debug("Serving resource '{0}'".format(self.path))

self.send_response(200) # 200 OK
self.send_response(200) # 200 OK

SimpleHTTPRequestHandler.do_GET(self)
SimpleHTTPRequestHandler.do_GET(self) # Actual serving of file.

@staticmethod
def __check_prod_db(product):
Expand Down
2 changes: 1 addition & 1 deletion libcodechecker/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

# The name of the cookie which contains the user's authentication session's
# token.
SESSION_COOKIE_NAME = "__ccPrivilegedAccessToken"
SESSION_COOKIE_NAME = '__ccPrivilegedAccessToken'

# The newest supported minor version (value) for each supported major version
# (key) in this particular build.
Expand Down
Loading