Skip to content

Commit 9008b1e

Browse files
authored
Add ADMIN roles through config file (#3475)
* Add ADMIN roles through config file PBENCH-1197 With the change to SSO, we've lost our own private realm and the ability to manage roles within it. We may be able to restore roles through Keycloak and LDAP groups, but this provides a temporary "quick and dirty" mechanism to define ADMIN roles for a server based on the username provided and cached from OIDC tokens. We define a simple `admin-role` config variable which can be defined to a comma-separated list of usernames to be granted ADMIN role. This value is processed by the authorization code when it caches local `User` objects for validation. I've added a functional test for the `audit` API, which requires ADMIN role, both to prove that it works and to provide a long-delayed minimal validation of auditing. (I decided not to merge this into the overloaded "datasets" test file, which means it can't easily be run last: this makes it a less rigorous "audit test", but that can be addressed later and it provides an "ADMIN role test" that's necessary now.)
1 parent 75d4d91 commit 9008b1e

File tree

9 files changed

+175
-28
lines changed

9 files changed

+175
-28
lines changed

jenkins/run-server-func-tests

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export PB_SERVER_IMAGE_TAG=${PB_SERVER_IMAGE_TAG:-"$(cat jenkins/branch.name)"}
88
export PB_POD_NAME=${PB_POD_NAME:-"pbench-in-a-can_${PB_SERVER_IMAGE_TAG}"}
99
export PB_SERVER_CONTAINER_NAME=${PB_SERVER_CONTAINER_NAME:-"${PB_POD_NAME}-pbenchserver"}
1010

11+
# For functional testing, assign ADMIN role to the testadmin username
12+
export PB_ADMIN_NAMES=${PB_ADMIN_NAMES:-testadmin}
13+
1114
SERVER_URL="https://localhost:8443"
1215
SERVER_API_ENDPOINTS="${SERVER_URL}/api/v1/endpoints"
1316

lib/pbench/server/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ def shutdown_session(exception=None):
214214
app = Flask(__name__.split(".")[0])
215215
CORS(app, resources={r"/api/*": {"origins": "*"}})
216216

217+
app.server_config = server_config
217218
app.logger = get_pbench_logger(__name__, server_config)
218219

219220
Auth.setup_app(app, server_config)

lib/pbench/server/auth/auth.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pbench.server import PbenchServerConfig
99
from pbench.server.auth import OpenIDClient
1010
from pbench.server.database.models.api_keys import APIKey
11-
from pbench.server.database.models.users import User
11+
from pbench.server.database.models.users import Roles, User
1212

1313
# Module public
1414
token_auth = HTTPTokenAuth("Bearer")
@@ -159,6 +159,12 @@ def verify_auth_oidc(auth_token: str) -> Optional[User]:
159159
audiences = token_payload.get("resource_access", {})
160160
pb_aud = audiences.get(oidc_client.client_id, {})
161161
roles = pb_aud.get("roles", [])
162+
if Roles.ADMIN.name not in roles:
163+
admin_users = current_app.server_config.get(
164+
"pbench-server", "admin-role", fallback=""
165+
)
166+
if username in admin_users.split(","):
167+
roles.append(Roles.ADMIN.name)
162168

163169
# Create or update the user in our cache
164170
user = User.query(id=user_id)

lib/pbench/test/functional/server/conftest.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,24 @@
55

66
from pbench.client import PbenchServerClient
77
from pbench.client.oidc_admin import OIDCAdmin
8+
from pbench.client.types import JSONOBJECT
89
from pbench.server.auth import OpenIDClientError
910

10-
USERNAME: str = "tester"
11-
EMAIL: str = "[email protected]"
12-
PASSWORD: str = "123456"
13-
FIRST_NAME: str = "Test"
14-
LAST_NAME: str = "User"
11+
USER = {
12+
"username": "tester",
13+
"email": "[email protected]",
14+
"password": "123456",
15+
"first_name": "Test",
16+
"last_name": "User",
17+
}
18+
19+
ADMIN = {
20+
"username": "testadmin",
21+
"email": "[email protected]",
22+
"password": "123456",
23+
"first_name": "Admin",
24+
"last_name": "Tester",
25+
}
1526

1627

1728
@pytest.fixture(scope="session")
@@ -41,17 +52,9 @@ def oidc_admin(server_client: PbenchServerClient):
4152
return OIDCAdmin(server_url=server_client.endpoints["openid"]["server"])
4253

4354

44-
@pytest.fixture(scope="session")
45-
def register_test_user(oidc_admin: OIDCAdmin):
46-
"""Create a test user for functional tests."""
55+
def register_user(oidc_admin: OIDCAdmin, user: JSONOBJECT):
4756
try:
48-
response = oidc_admin.create_new_user(
49-
username=USERNAME,
50-
email=EMAIL,
51-
password=PASSWORD,
52-
first_name=FIRST_NAME,
53-
last_name=LAST_NAME,
54-
)
57+
response = oidc_admin.create_new_user(**user)
5558
except OpenIDClientError as e:
5659
# To allow testing outside our transient CI containers, allow the tester
5760
# user to already exist.
@@ -62,10 +65,31 @@ def register_test_user(oidc_admin: OIDCAdmin):
6265
assert response.ok, f"Register failed with {response.json()}"
6366

6467

68+
@pytest.fixture(scope="session")
69+
def register_test_user(oidc_admin: OIDCAdmin):
70+
"""Create a test user for functional tests."""
71+
register_user(oidc_admin, USER)
72+
73+
74+
@pytest.fixture(scope="session")
75+
def register_admintest_user(oidc_admin: OIDCAdmin):
76+
"""Create a test user matching the configured Pbench admin."""
77+
register_user(oidc_admin, ADMIN)
78+
79+
6580
@pytest.fixture
6681
def login_user(server_client: PbenchServerClient, register_test_user):
6782
"""Log in the test user and return the authentication token"""
68-
server_client.login(USERNAME, PASSWORD)
83+
server_client.login(USER["username"], USER["password"])
84+
assert server_client.auth_token
85+
yield
86+
server_client.auth_token = None
87+
88+
89+
@pytest.fixture
90+
def login_admin(server_client: PbenchServerClient, register_admintest_user):
91+
"""Log in the test user and return the authentication token"""
92+
server_client.login(ADMIN["username"], ADMIN["password"])
6993
assert server_client.auth_token
7094
yield
7195
server_client.auth_token = None
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from pbench.client import API, PbenchServerClient
2+
3+
4+
class TestAudit:
5+
def test_get_all(self, server_client: PbenchServerClient, login_admin):
6+
"""
7+
Verify that we can retrieve the Pbench Server audit log.
8+
9+
This relies on a "testadmin" user which has been granted ADMIN role
10+
via the pbench-server.cfg file for functional testing. The audit API
11+
should succeed without permissions failure, and we'll validate the
12+
audit fields of the records we see.
13+
"""
14+
response = server_client.get(API.SERVER_AUDIT, {})
15+
json = response.json()
16+
assert (
17+
response.ok
18+
), f"Reading audit log failed {response.status_code},{json['message']}"
19+
assert isinstance(json, list)
20+
print(f" ... read {len(json)} audit records")
21+
for audit in json:
22+
assert isinstance(audit["id"], int)
23+
assert audit["name"]
24+
assert audit["operation"] in ("CREATE", "READ", "UPDATE", "DELETE")
25+
assert audit["reason"] in (None, "PERMISSION", "INTERNAL", "CONSISTENCY")
26+
assert "root_id" in audit
27+
if audit["root_id"]:
28+
assert isinstance(audit["root_id"], int)
29+
assert audit["status"] in ("BEGIN", "SUCCESS", "FAILURE", "WARNING")
30+
assert audit["timestamp"]
31+
assert audit["attributes"]
32+
assert audit["object_type"] in (
33+
"API_KEY",
34+
"CONFIG",
35+
"DATASET",
36+
"NONE",
37+
"TEMPLATE",
38+
)
39+
if audit["object_type"] != "NONE":
40+
assert audit["object_name"]
41+
if audit["object_type"] == "DATASET":
42+
assert audit["object_id"]
43+
if audit["user_name"] not in (None, "BACKGROUND"):
44+
assert audit["user_id"]

lib/pbench/test/unit/server/auth/test_auth.py

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from requests.structures import CaseInsensitiveDict
1111
import responses
1212

13-
from pbench.server import JSON
13+
from pbench.server import JSON, PbenchServerConfig
1414
import pbench.server.auth
1515
from pbench.server.auth import Connection, OpenIDClient, OpenIDClientError
1616
import pbench.server.auth.auth as Auth
@@ -529,7 +529,9 @@ def test_get_auth_token_succ(self, monkeypatch, make_logger):
529529
"headers",
530530
[{"Authorization": "not-bearer my-token"}, {"Authorization": "no-space"}, {}],
531531
)
532-
def test_get_auth_token_fail(self, monkeypatch, make_logger, headers):
532+
def test_get_auth_token_fail(
533+
self, monkeypatch, server_config, make_logger, headers
534+
):
533535
"""Verify error handling fetching the authorization token from HTTP
534536
headers
535537
"""
@@ -548,6 +550,7 @@ def record_abort(code: int, message: str = ""):
548550

549551
app = Flask("test-get-auth-token-fail")
550552
app.logger = make_logger
553+
app.server_config = server_config
551554
with app.app_context():
552555
monkeypatch.setattr(Auth, "request", MockRequest(headers=headers))
553556
try:
@@ -563,36 +566,41 @@ def record_abort(code: int, message: str = ""):
563566
)
564567
assert record["code"] == expected_code
565568

566-
def test_verify_auth(self, make_logger, pbench_drb_token):
569+
def test_verify_auth(self, server_config, make_logger, pbench_drb_token):
567570
"""Verify success path of verify_auth"""
568571
app = Flask("test-verify-auth")
569572
app.logger = make_logger
573+
app.server_config = server_config
570574
with app.app_context():
571575
current_app.secret_key = jwt_secret
572576
user = Auth.verify_auth(pbench_drb_token)
573577
assert user.id == DRB_USER_ID
574578

575-
def test_verify_auth_invalid(self, make_logger, pbench_drb_token_invalid):
579+
def test_verify_auth_invalid(
580+
self, server_config, make_logger, pbench_drb_token_invalid
581+
):
576582
"""Verify handling of an invalid (expired) token in verify_auth"""
577583
app = Flask("test-verify-auth-invalid")
578584
app.logger = make_logger
585+
app.server_config = server_config
579586
with app.app_context():
580587
current_app.secret_key = jwt_secret
581588
user = Auth.verify_auth(pbench_drb_token_invalid)
582589
assert user is None
583590

584-
def test_verify_auth_invsig(self, make_logger, pbench_drb_token):
591+
def test_verify_auth_invsig(self, server_config, make_logger, pbench_drb_token):
585592
"""Verify handling of a token with an invalid signature"""
586593
app = Flask("test-verify-auth-invsig")
587594
app.logger = make_logger
595+
app.server_config = server_config
588596
with app.app_context():
589597
current_app.secret_key = jwt_secret
590598
user = Auth.verify_auth(pbench_drb_token + "1")
591599
assert user is None
592600

593601
@pytest.mark.parametrize("roles", [["ROLE"], ["ROLE1", "ROLE2"], [], None])
594602
def test_verify_auth_oidc(
595-
self, monkeypatch, db_session, rsa_keys, make_logger, roles
603+
self, monkeypatch, server_config, db_session, rsa_keys, make_logger, roles
596604
):
597605
"""Verify OIDC token offline verification success path"""
598606
client_id = "us"
@@ -613,14 +621,15 @@ def test_verify_auth_oidc(
613621

614622
app = Flask("test-verify-auth-oidc")
615623
app.logger = make_logger
624+
app.server_config = server_config
616625
with app.app_context():
617626
user = Auth.verify_auth(token)
618627

619628
assert user.id == "12345"
620629
assert user.roles == (roles if roles else [])
621630

622631
def test_verify_auth_oidc_user_update(
623-
self, monkeypatch, db_session, rsa_keys, make_logger
632+
self, monkeypatch, server_config, db_session, rsa_keys, make_logger
624633
):
625634
"""Verify we update our internal user database when we get updated user
626635
payload from the OIDC token for an existing user."""
@@ -637,14 +646,15 @@ def test_verify_auth_oidc_user_update(
637646

638647
app = Flask("test-verify-auth-oidc-user-update")
639648
app.logger = make_logger
649+
app.server_config = server_config
640650
with app.app_context():
641651
user = Auth.verify_auth(token)
642652

643653
assert user.id == "12345"
644654
assert user.roles == []
645655
assert user.username == "dummy"
646656

647-
# Generate a new token with a role for the same user
657+
# Generate a token with a new username for the same UUID
648658
token, expected_payload = gen_rsa_token(
649659
client_id,
650660
rsa_keys["private_key"],
@@ -657,7 +667,54 @@ def test_verify_auth_oidc_user_update(
657667
assert user.roles == ["ROLE"]
658668
assert user.username == "new_dummy"
659669

660-
def test_verify_auth_oidc_invalid(self, monkeypatch, rsa_keys, make_logger):
670+
def test_verify_auth_oidc_user_admin(
671+
self,
672+
monkeypatch,
673+
server_config: PbenchServerConfig,
674+
db_session,
675+
rsa_keys,
676+
make_logger,
677+
):
678+
"""Verify we update our internal user database when we get updated user
679+
payload from the OIDC token for an existing user."""
680+
client_id = "us"
681+
token, expected_payload = gen_rsa_token(client_id, rsa_keys["private_key"])
682+
683+
# Mock the Connection object and generate an OpenIDClient object,
684+
# installing it as Auth module's OIDC client.
685+
config = mock_connection(
686+
monkeypatch, client_id, public_key=rsa_keys["public_key"]
687+
)
688+
oidc_client = OpenIDClient.construct_oidc_client(config)
689+
monkeypatch.setattr(Auth, "oidc_client", oidc_client)
690+
server_config._conf.set("pbench-server", "admin-role", "friend,dummy,admin")
691+
692+
app = Flask("test-verify-auth-oidc-user-admin")
693+
app.logger = make_logger
694+
app.server_config = server_config
695+
with app.app_context():
696+
user = Auth.verify_auth(token)
697+
698+
assert user.id == "12345"
699+
assert user.roles == ["ADMIN"]
700+
assert user.username == "dummy"
701+
702+
# Generate a token with a role and new username for the same UUID
703+
token, expected_payload = gen_rsa_token(
704+
client_id,
705+
rsa_keys["private_key"],
706+
username="friend",
707+
oidc_client_roles=["ROLE"],
708+
)
709+
with app.app_context():
710+
user = Auth.verify_auth(token)
711+
assert user.id == "12345"
712+
assert user.roles == ["ROLE", "ADMIN"]
713+
assert user.username == "friend"
714+
715+
def test_verify_auth_oidc_invalid(
716+
self, monkeypatch, server_config, rsa_keys, make_logger
717+
):
661718
"""Verify OIDC token offline verification via Auth.verify_auth() fails
662719
gracefully with an invalid token
663720
"""
@@ -677,14 +734,15 @@ def tio_exc(token: str) -> JSON:
677734

678735
app = Flask("test-verify-auth-oidc-invalid")
679736
app.logger = make_logger
737+
app.server_config = server_config
680738
with app.app_context():
681739
monkeypatch.setattr(oidc_client, "token_introspect", tio_exc)
682740
user = Auth.verify_auth(token)
683741

684742
assert user is None
685743

686744
def test_verify_auth_api_key(
687-
self, monkeypatch, rsa_keys, make_logger, pbench_drb_api_key
745+
self, monkeypatch, server_config, rsa_keys, make_logger, pbench_drb_api_key
688746
):
689747
"""Verify api_key verification via Auth.verify_auth()"""
690748

@@ -699,14 +757,15 @@ def tio_exc(token: str) -> JSON:
699757

700758
app = Flask("test_verify_auth_api_key")
701759
app.logger = make_logger
760+
app.server_config = server_config
702761
with app.app_context():
703762
monkeypatch.setattr(oidc_client, "token_introspect", tio_exc)
704763
current_app.secret_key = jwt_secret
705764
user = Auth.verify_auth(pbench_drb_api_key.key)
706765
assert user.id == DRB_USER_ID
707766

708767
def test_verify_auth_api_key_invalid(
709-
self, monkeypatch, rsa_keys, make_logger, pbench_invalid_api_key
768+
self, monkeypatch, server_config, rsa_keys, make_logger, pbench_invalid_api_key
710769
):
711770
"""Verify api_key verification via Auth.verify_auth() fails
712771
gracefully with an invalid token
@@ -722,6 +781,7 @@ def tio_exc(token: str) -> JSON:
722781

723782
app = Flask("test_verify_auth_api_key_invalid")
724783
app.logger = make_logger
784+
app.server_config = server_config
725785
with app.app_context():
726786
monkeypatch.setattr(oidc_client, "token_introspect", tio_exc)
727787
current_app.secret_key = jwt_secret

server/lib/config/pbench-server-default.cfg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ admin-email=%(user)s@localhost
2525
mailto=%(admin-email)s
2626
mailfrom=%(user)s@localhost
2727

28+
# A comma-separated list of OIDC usernames with no spaces. These usernames
29+
# will be granted ADMIN access on the server. These are OIDC ID provider
30+
# usernames matched against decrypted authorization tokens. If no usernames
31+
# are specified, no users have ADMIN access. NOTE: this is a temporary measure
32+
# until we work out Keycloak / LDAP roles.
33+
#admin-role=user1,user2
34+
2835
# Token expiration duration in minutes, can be overridden in the main config file, defaults to 60 mins
2936
token_expiration_duration = 60
3037

server/pbenchinacan/etc/pbench-server/pbench-server.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ realhost = pbenchinacan
1212
maximum-dataset-retention-days = 36500
1313
default-dataset-retention-days = 730
1414
roles = pbench-results
15+
admin-role = ##ADMIN_NAMES##
1516

1617
[Indexing]
1718
index_prefix = container-pbench

0 commit comments

Comments
 (0)