Repository: holoviz/panel

Issue:
auth checks
#### ALL software version info

- panel main
- bokeh 3.2.2
- python 3.9.17
- juptyerlab 3.6.5
- linux

#### Description of expected behavior and the observed behavior

##### Expected behavior

#5386 adds the ability to authenticate user names and _optionally_ user paths in a Panel app. If a user does not create an `authorize_callback` method that takes two parameters, then the new optional user path checks for authentication should be ignored.

##### Observed behavior

A follow up comment https://github.com/holoviz/panel/pull/5386#issuecomment-1714586556 from @el-abcd indicates that the feature errors when an `authorize_callback` object only has one parameter. @maximlt also confirmed the bug in the conversation.

#### Complete, minimal, self-contained example code that reproduces the issue

```python
# app1.py
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import panel as pn
import param
from auth import check_user_authorization

pn.extension()
pn.config.authorize_callback = check_user_authorization

class App1(param.Parameterized):
    a = param.Integer(default=2, bounds=(0, 10))
    b = param.Integer(default=3, bounds=(0, 10))

    def view(self):
        return pn.pane.HTML(f"<p>a={self.a} b={self.b}</p>")

    def panel(self):
        return pn.Row(self.param, self.view).servable()

app1 = App1()
app1.panel()
```

```python
# app2.py
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import panel as pn
import param
from auth import check_user_authorization

pn.extension()
pn.config.authorize_callback = check_user_authorization

class App2(param.Parameterized):
    c = param.Integer(default=6, bounds=(0, None))
    exp = param.Number(default=0.1, bounds=(0, 3))

    def view(self):
        out = self.c**self.exp
        return pn.Column(out)

    def panel(self):
        return pn.Row(self.param, self.view).servable()

app2 = App2()
app2.panel()
```

```python
# auth.py
from typing import Any

authorized_users = {"admin", "user"}

def check_user_authorization(user_info: dict[str, Any]) -> bool:
    authenticated = False
    current_user = user_info["user"]
    if current_user in authorized_users:
        authenticated = True
    return authenticated
```

```json5
// credentials.json
{
  "user": "user",
  "admin": "admin"
}
```

Run the apps via the command line

```bash
panel serve app1.py app2.py --basic-auth credentials.json --cookie-secret my_super_safe_cookie_secret
```

#### Stack traceback and/or browser JavaScript console output

```bash
$ panel serve app1.py app2.py --basic-auth credentials.json --cookie-secret my_super_safe_cookie_secret
2023-09-12 10:27:49,876 Starting Bokeh server version 3.2.2 (running on Tornado 6.3.2)
2023-09-12 10:27:49,877 User authentication hooks provided (no default user)
2023-09-12 10:27:49,880 Bokeh app running at: http://localhost:5006/app1
2023-09-12 10:27:49,880 Bokeh app running at: http://localhost:5006/app2
2023-09-12 10:27:49,880 Starting Bokeh server with process id: 3589592
2023-09-12 10:28:00,772 404 GET /thumbnails/app1.png (127.0.0.1) 1.09ms
2023-09-12 10:28:00,774 404 GET /thumbnails/app2.png (127.0.0.1) 1.25ms
2023-09-12 10:28:18,725 404 GET /thumbnails/app1.png (127.0.0.1) 1.46ms
2023-09-12 10:28:18,727 404 GET /thumbnails/app2.png (127.0.0.1) 1.07ms
2023-09-12 10:28:22,158 Uncaught exception GET /app1 (127.0.0.1)
HTTPServerRequest(protocol='http', host='localhost:5006', method='GET', uri='/app1', version='HTTP/1.1', remote_ip='127.0.0.1')
Traceback (most recent call last):
  File "$HOME/mambaforge/envs/panel-dev/lib/python3.9/site-packages/tornado/web.py", line 1786, in _execute
    result = await result
  File "PATH/TO/PANEL-INSTALL/panel/panel/io/server.py", line 517, in get
    raise RuntimeError(
RuntimeError: Authorization callback must accept either one or two arguments.
2023-09-12 10:28:22,159 500 GET /app1 (127.0.0.1) 305.37ms
```

#### Screenshots or screencasts of the bug in action

![image](https://github.com/holoviz/panel/assets/60523020/0eef86a5-ea64-4549-8987-86bf60acab34)


Discussion:

Resolving patch:
diff --git a/panel/io/server.py b/panel/io/server.py
--- a/panel/io/server.py
+++ b/panel/io/server.py
@@ -507,22 +507,32 @@ async def get(self, *args, **kwargs):
             with set_curdoc(session.document):
                 resources = Resources.from_bokeh(self.application.resources())
                 auth_cb = config.authorize_callback
+                authorized = False
                 if auth_cb:
                     auth_cb = config.authorize_callback
                     auth_params = inspect.signature(auth_cb).parameters
-                    auth_args = (state.user_info,)
-                    if len(auth_params) == 2:
-                        auth_args += (self.request.path,)
+                    if len(auth_params) == 1:
+                        auth_args = (state.user_info,)
+                    elif len(auth_params) == 2:
+                        auth_args = (state.user_info, self.request.path,)
                     else:
                         raise RuntimeError(
-                            'Authorization callback must accept either one or two arguments.'
+                            'Authorization callback must accept either 1) a single argument '
+                            'which is the user name or 2) two arguments which includes the '
+                            'user name and the url path the user is trying to access.'
                         )
                     auth_error = f'{state.user} is not authorized to access this application.'
                     try:
                         authorized = auth_cb(*auth_args)
-                        auth_error = None
+                        if not authorized:
+                            auth_error = (
+                                f'Authorization callback errored. Could not validate user name "{state.user}" '
+                                f'for the given app "{self.request.path}".'
+                            )
+                        if authorized:
+                            auth_error = None
                     except Exception:
-                        auth_error = f'Authorization callback errored. Could not validate user {state.user}'
+                        auth_error = f'Authorization callback errored. Could not validate user {state.user}.'
                 else:
                     authorized = True
 


Test patch: