Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OIDC user to the functional test #3235

Merged
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
46 changes: 23 additions & 23 deletions lib/pbench/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import requests
from requests.structures import CaseInsensitiveDict

from pbench.client.types import Dataset, JSONMap, JSONOBJECT
from pbench.client.oidc_admin import OIDCAdmin
from pbench.client.types import Dataset, JSONOBJECT


class PbenchClientError(Exception):
Expand Down Expand Up @@ -86,6 +87,7 @@ def __init__(self, host: str):
self.auth_token: Optional[str] = None
self.session: Optional[requests.Session] = None
self.endpoints: Optional[JSONOBJECT] = None
self.oidc_admin: Optional[OIDCAdmin] = None

def _headers(
self, user_headers: Optional[dict[str, str]] = None
Expand Down Expand Up @@ -302,8 +304,13 @@ def delete(
return response

def connect(self, headers: Optional[dict[str, str]] = None) -> None:
"""Connect to the Pbench Server host using the endpoints API to be sure
that it responds, and cache the endpoints response payload.
"""Performs some pre-requisite actions to make server client usable.
1. Connect to the Pbench Server host using the endpoints API to be
sure that it responds, and cache the endpoints response payload.
2. Create an OIDCAdmin object that a server client can use to
perform privileged actions on an OIDC server.
This also allows the client to add default HTTP headers to the session
which will be used for all operations unless overridden for specific
Expand All @@ -321,31 +328,24 @@ def connect(self, headers: Optional[dict[str, str]] = None) -> None:
self.endpoints = response.json()
assert self.endpoints

def login(self, user: str, password: str) -> JSONMap:
"""Login to a specified username with the password, and store the
resulting authentication token.
# Create an OIDCAdmin object and confirm the connection was successful
self.oidc_admin = OIDCAdmin(server_url=self.endpoints["openid"]["server"])

def login(self, user: str, password: str):
"""Log into the OIDC server using the specified username and password,
and store the resulting authentication token.
Args:
user: Account username
password: Account password
Returns:
The login response
"""
response = self.post(API.LOGIN, json={"username": user, "password": password})
response.raise_for_status()
json = response.json()
self.username = json["username"]
self.auth_token = json["auth_token"]
return JSONMap(json)

def logout(self) -> None:
"""Logout the currently authenticated user and remove the
authentication token.
"""
self.post(API.LOGOUT)
self.username = None
self.auth_token = None
response = self.oidc_admin.user_login(
client_id=self.endpoints["openid"]["client"],
username=user,
password=password,
)
self.username = user
self.auth_token = response["access_token"]

def upload(self, tarball: Path, **kwargs) -> requests.Response:
"""Upload a tarball to the server.
Expand Down
141 changes: 141 additions & 0 deletions lib/pbench/client/oidc_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from http import HTTPStatus
import os

import requests

from pbench.server.auth import Connection


class OIDCAdmin(Connection):
webbnh marked this conversation as resolved.
Show resolved Hide resolved
OIDC_REALM = os.getenv("OIDC_REALM", "pbench-server")
ADMIN_USERNAME = os.getenv("OIDC_ADMIN_USERNAME", "admin")
ADMIN_PASSWORD = os.getenv("OIDC_ADMIN_PASSWORD", "admin")

def __init__(self, server_url: str):
super().__init__(server_url, verify=False)

def get_admin_token(self) -> dict:
"""pbench-server realm admin user login.
Returns:
access_token json payload
{ 'access_token': <access_token>,
'expires_in': 60, 'refresh_expires_in': 1800,
'refresh_token': <refresh_token>,
'token_type': 'Bearer',
'not-before-policy': 0,
'session_state': '8f558797-50e7-496d-bb45-3b5ac9fdcddb',
'scope': 'profile email'}
"""
url_path = "/realms/master/protocol/openid-connect/token"
data = {
"grant_type": "password",
"client_id": "admin-cli",
"username": self.ADMIN_USERNAME,
"password": self.ADMIN_PASSWORD,
}
return self.post(path=url_path, data=data).json()

def create_new_user(
self,
username: str,
email: str,
password: str,
first_name: str = "",
last_name: str = "",
) -> requests.Response:
"""Creates a new user under the OIDC_REALM.
Note: This involves a REST API call to the
OIDC server to create a new user.
Args:
username: username to register,
email: user email address,
password: user password,
first_name: Optional first name of the user,
last_name: Optional first name of the user,
Returns:
Response from the request.
"""
admin_token = self.get_admin_token().get("access_token")
url_path = f"/admin/realms/{self.OIDC_REALM}/users"
headers = {"Authorization": f"Bearer {admin_token}"}
data = {
"username": username,
"email": email,
"emailVerified": True,
"enabled": True,
"firstName": first_name,
"lastName": last_name,
"credentials": [
{"type": "password", "value": password, "temporary": False}
],
}
response = self.post(path=url_path, json=data, headers=headers)
return response

def user_login(self, client_id: str, username: str, password: str) -> dict:
"""pbench-server realm user login on a specified client.
Args:
client_id: client_name to use in the request
username: username of the user logging in
password: OIDC password
Returns:
access_token json payload
{ 'access_token': <access_token>,
'expires_in': 60, 'refresh_expires_in': 1800,
'refresh_token': <refresh_token>,
'token_type': 'Bearer',
'not-before-policy': 0,
'session_state': '8f558797-50e7-496d-bb45-3b5ac9fdcddb',
'scope': 'profile email'}
"""
url_path = f"/realms/{self.OIDC_REALM}/protocol/openid-connect/token"
data = {
"client_id": client_id,
"grant_type": "password",
"scope": "profile email",
"username": username,
"password": password,
}
return self.post(path=url_path, data=data).json()

def get_user(self, username: str, token: str) -> dict:
"""Get the OIDC user representation dict.
Args:
username: username to query
token: access_token string to validate
Returns:
User dict representation
{'id': '37117992-a3de-43f7-b844-e6ee178e9965',
'createdTimestamp': 1675981768951,
'username': 'admin',
'enabled': True,
'totp': False,
'emailVerified': False,
'disableableCredentialTypes': [],
'requiredActions': [],
'notBefore': 0,
'access': {'manageGroupMembership': True, 'view': True, 'mapRoles': True, 'impersonate': True, 'manage': True}
...
}
"""
response = self.get(
f"admin/realms/{self.OIDC_REALM}/users",
headers={"Authorization": f"Bearer {token}"},
username=username,
dbutenhof marked this conversation as resolved.
Show resolved Hide resolved
)
if response.status_code == HTTPStatus.OK:
return response.json()[0]
return {}
20 changes: 6 additions & 14 deletions lib/pbench/server/api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1231,13 +1231,6 @@ def _check_authorization(self, mode: ApiAuthorization):
user_id = mode.user
role = mode.role
authorized_user: User = Auth.token_auth.current_user()
username = "none"
if user_id:
user = User.query(id=user_id)
if user:
username = user.username
else:
current_app.logger.error("User ID {} not found", user_id)

# The ADMIN authorization doesn't involve a target resource owner or
# access, so take care of that first as a special case. If there is
Expand All @@ -1260,10 +1253,9 @@ def _check_authorization(self, mode: ApiAuthorization):
access = mode.access

current_app.logger.debug(
"Authorizing {} access for {} to user {} ({}) with access {} using {}",
"Authorizing {} access for {} to user (user id: {}) with access {} using {}",
role,
authorized_user,
username,
user_id,
mode.access,
mode.type,
Expand All @@ -1290,12 +1282,12 @@ def _check_authorization(self, mode: ApiAuthorization):
# An unauthenticated user is never allowed to access private
# data nor to perform an potential mutation of data: REJECT
current_app.logger.warning(
"Attempt to {} user {} data without login", role, username
"Attempt to {} user {} data without login", role, user_id
)
raise UnauthorizedAccess(
authorized_user,
role,
username,
user_id,
dbutenhof marked this conversation as resolved.
Show resolved Hide resolved
access,
HTTPStatus.UNAUTHORIZED,
)
Expand All @@ -1308,7 +1300,7 @@ def _check_authorization(self, mode: ApiAuthorization):
role,
)
raise UnauthorizedAccess(
authorized_user, role, username, access, HTTPStatus.FORBIDDEN
authorized_user, role, user_id, access, HTTPStatus.FORBIDDEN
)
elif (
user_id
Expand All @@ -1322,10 +1314,10 @@ def _check_authorization(self, mode: ApiAuthorization):
"Unauthorized attempt by {} to {} user {} data",
authorized_user,
role,
username,
user_id,
)
raise UnauthorizedAccess(
authorized_user, role, username, access, HTTPStatus.FORBIDDEN
authorized_user, role, user_id, access, HTTPStatus.FORBIDDEN
)
else:
# We have determined that there is an authenticated user with
Expand Down
Loading