Skip to content

Commit fc7f0cb

Browse files
committed
Merge branch 'main' into feature/adfs-unitest
Signed-off-by: calculus-ask <a.santhana.k@gmail.com>
2 parents 8f55abe + 1404dbf commit fc7f0cb

23 files changed

Lines changed: 3053 additions & 184 deletions

.secrets.baseline

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "package-lock.json|Cargo.lock|^.secrets.baseline$|scripts/sign_image.sh|scripts/zap|sonar-project.properties|^/Users/brian/dev/github.ibm.com/contextforge-org/sps-pipeline-config/.secrets.baseline$|^./.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2026-03-30T18:16:29Z",
6+
"generated_at": "2026-03-30T23:20:07Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -5078,31 +5078,31 @@
50785078
"hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3",
50795079
"is_secret": false,
50805080
"is_verified": false,
5081-
"line_number": 4194,
5081+
"line_number": 4195,
50825082
"type": "Secret Keyword",
50835083
"verified_result": null
50845084
},
50855085
{
50865086
"hashed_secret": "559b05f1b2863e725b76e216ac3dadecbf92e244",
50875087
"is_secret": false,
50885088
"is_verified": false,
5089-
"line_number": 4802,
5089+
"line_number": 4803,
50905090
"type": "Secret Keyword",
50915091
"verified_result": null
50925092
},
50935093
{
50945094
"hashed_secret": "a8af4759392d4f7496d613174f33afe2074a4b8d",
50955095
"is_secret": false,
50965096
"is_verified": false,
5097-
"line_number": 4804,
5097+
"line_number": 4805,
50985098
"type": "Secret Keyword",
50995099
"verified_result": null
51005100
},
51015101
{
51025102
"hashed_secret": "85b60d811d16ff56b3654587d4487f713bfa33b7",
51035103
"is_secret": false,
51045104
"is_verified": false,
5105-
"line_number": 14866,
5105+
"line_number": 14867,
51065106
"type": "Secret Keyword",
51075107
"verified_result": null
51085108
}
@@ -6571,6 +6571,24 @@
65716571
"verified_result": null
65726572
}
65736573
],
6574+
"plugins/encoded_exfil_detection/README.md": [
6575+
{
6576+
"hashed_secret": "f7f877c24a4ebef314009a2e79314797bae91bb6",
6577+
"is_secret": false,
6578+
"is_verified": false,
6579+
"line_number": 149,
6580+
"type": "Secret Keyword",
6581+
"verified_result": null
6582+
},
6583+
{
6584+
"hashed_secret": "5f2fe4f03b2314fa3a217f1e1f0ab2e3f5b1b49f",
6585+
"is_secret": false,
6586+
"is_verified": false,
6587+
"line_number": 178,
6588+
"type": "Secret Keyword",
6589+
"verified_result": null
6590+
}
6591+
],
65746592
"plugins/examples/custom_auth_example/README.md": [
65756593
{
65766594
"hashed_secret": "423529858c0ba74fbbe141bce50b20f3c0d3b9f6",
@@ -6788,31 +6806,55 @@
67886806
"hashed_secret": "1d278d3c888d1a2fa7eed622bfc02927ce4049af",
67896807
"is_secret": false,
67906808
"is_verified": false,
6791-
"line_number": 468,
6809+
"line_number": 573,
67926810
"type": "Base64 High Entropy String",
67936811
"verified_result": null
67946812
},
67956813
{
67966814
"hashed_secret": "d7da4707f9793b357da965eb04f06ceae7d7bfa6",
67976815
"is_secret": false,
67986816
"is_verified": false,
6799-
"line_number": 469,
6817+
"line_number": 574,
68006818
"type": "Base64 High Entropy String",
68016819
"verified_result": null
68026820
},
68036821
{
68046822
"hashed_secret": "0962e90797cdf7ff77c8a0a9477a7ca4f493465f",
68056823
"is_secret": false,
68066824
"is_verified": false,
6807-
"line_number": 649,
6825+
"line_number": 892,
68086826
"type": "Secret Keyword",
68096827
"verified_result": null
68106828
},
68116829
{
68126830
"hashed_secret": "ca3f3092640b07c051f03e52690fda1bcee3f60d",
68136831
"is_secret": false,
68146832
"is_verified": false,
6815-
"line_number": 670,
6833+
"line_number": 913,
6834+
"type": "Secret Keyword",
6835+
"verified_result": null
6836+
},
6837+
{
6838+
"hashed_secret": "532fdeccb155dce5b528ba039b9a5d201b817e3b",
6839+
"is_secret": false,
6840+
"is_verified": false,
6841+
"line_number": 932,
6842+
"type": "Secret Keyword",
6843+
"verified_result": null
6844+
},
6845+
{
6846+
"hashed_secret": "cf743b3a58a4d0f91c1d7f5825c0b1b5f7758174",
6847+
"is_secret": false,
6848+
"is_verified": false,
6849+
"line_number": 969,
6850+
"type": "Base64 High Entropy String",
6851+
"verified_result": null
6852+
},
6853+
{
6854+
"hashed_secret": "ff1a7ebc241b8eefe9e75e13f20cc22c365ab626",
6855+
"is_secret": false,
6856+
"is_verified": false,
6857+
"line_number": 969,
68166858
"type": "Secret Keyword",
68176859
"verified_result": null
68186860
}
@@ -8274,15 +8316,15 @@
82748316
"hashed_secret": "ab73a3eaca01a7059dcdff6f95556ec7fd83de96",
82758317
"is_secret": false,
82768318
"is_verified": false,
8277-
"line_number": 1332,
8319+
"line_number": 1388,
82788320
"type": "Secret Keyword",
82798321
"verified_result": null
82808322
},
82818323
{
82828324
"hashed_secret": "92dd4a2de441b63e6ac51fdb81a05a416dffd182",
82838325
"is_secret": false,
82848326
"is_verified": false,
8285-
"line_number": 1419,
8327+
"line_number": 1475,
82868328
"type": "Secret Keyword",
82878329
"verified_result": null
82888330
}
@@ -10722,7 +10764,7 @@
1072210764
"hashed_secret": "6745fa570d36be08400efa1cbc2f057bb001290e",
1072310765
"is_secret": false,
1072410766
"is_verified": false,
10725-
"line_number": 2225,
10767+
"line_number": 2233,
1072610768
"type": "Hex High Entropy String",
1072710769
"verified_result": null
1072810770
},
@@ -11168,9 +11210,49 @@
1116811210
"hashed_secret": "55d2534ed6ad4f269b428160428fa2f6f541ba7b",
1116911211
"is_secret": false,
1117011212
"is_verified": false,
11171-
"line_number": 143,
11213+
"line_number": 156,
11214+
"type": "Base64 High Entropy String",
11215+
"verified_result": null
11216+
},
11217+
{
11218+
"hashed_secret": "cf743b3a58a4d0f91c1d7f5825c0b1b5f7758174",
11219+
"is_secret": false,
11220+
"is_verified": false,
11221+
"line_number": 538,
1117211222
"type": "Base64 High Entropy String",
1117311223
"verified_result": null
11224+
},
11225+
{
11226+
"hashed_secret": "8e42b03e460b2cf358ffbcf4da3bc5d14a22c86e",
11227+
"is_secret": false,
11228+
"is_verified": false,
11229+
"line_number": 582,
11230+
"type": "Base64 High Entropy String",
11231+
"verified_result": null
11232+
},
11233+
{
11234+
"hashed_secret": "2093dd9cf307518cfe1d2fa5a3985d6fec4e995e",
11235+
"is_secret": false,
11236+
"is_verified": false,
11237+
"line_number": 595,
11238+
"type": "Base64 High Entropy String",
11239+
"verified_result": null
11240+
},
11241+
{
11242+
"hashed_secret": "caa924f200b35ceb6f0e33878faff75203bdccb4",
11243+
"is_secret": false,
11244+
"is_verified": false,
11245+
"line_number": 971,
11246+
"type": "Secret Keyword",
11247+
"verified_result": null
11248+
},
11249+
{
11250+
"hashed_secret": "f16da2820437f3c703ff5b95c813f310ce8e67a4",
11251+
"is_secret": false,
11252+
"is_verified": false,
11253+
"line_number": 1288,
11254+
"type": "Secret Keyword",
11255+
"verified_result": null
1117411256
}
1117511257
],
1117611258
"tests/unit/plugins/test_jwt_claims_extraction.py": [

mcpgateway/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ async def get_hidden_sections_for_user(
507507
user_email=user_email,
508508
team_id=None, # Check across all teams
509509
include_all_teams=True, # Include all team-scoped roles
510+
token_teams=token_teams, # SECURITY: Respect token narrowing for menu visibility
510511
)
511512
if inspect.isawaitable(maybe_permissions):
512513
user_permissions = await maybe_permissions

mcpgateway/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2676,7 +2676,7 @@ async def dispatch(self, request: Request, call_next): # pylint: disable=too-ma
26762676
pass # keep raw value for non-UUID token_teams
26772677
# Only trust team_id if it is in the user's DB-resolved teams
26782678
validated_team_id = request_team_id if (token_teams and request_team_id and request_team_id in token_teams) else None
2679-
has_admin_access = await permission_service.has_admin_permission(username, team_id=validated_team_id)
2679+
has_admin_access = await permission_service.has_admin_permission(username, team_id=validated_team_id, token_teams=token_teams)
26802680
if not has_admin_access:
26812681
logger.warning(f"Admin access denied for user without admin permissions: {SecurityValidator.sanitize_log_message(str(username))}")
26822682
return self._error_response(request, root_path, 403, "Admin privileges required", "admin_required")

mcpgateway/middleware/rbac.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ def require_admin_permission():
739739
>>> class DummyPS:
740740
... def __init__(self, db):
741741
... pass
742-
... async def check_admin_permission(self, email):
742+
... async def check_admin_permission(self, email, token_teams=None):
743743
... return True
744744
>>> @require_admin_permission()
745745
... async def demo(user=None):
@@ -781,15 +781,16 @@ async def wrapper(*args, **kwargs):
781781

782782
# Get db session: prefer endpoint's db param, then user_context["db"], then create fresh
783783
db_session = kwargs.get("db") or user_context.get("db")
784+
token_teams = user_context.get("token_teams") # Forward token scope
784785
if db_session:
785786
# Use existing session from endpoint or user_context
786787
permission_service = PermissionService(db_session)
787-
has_admin_permission = await permission_service.check_admin_permission(user_context["email"])
788+
has_admin_permission = await permission_service.check_admin_permission(user_context["email"], token_teams=token_teams)
788789
else:
789790
# Create fresh db session for permission check
790791
with fresh_db_session() as db:
791792
permission_service = PermissionService(db)
792-
has_admin_permission = await permission_service.check_admin_permission(user_context["email"])
793+
has_admin_permission = await permission_service.check_admin_permission(user_context["email"], token_teams=token_teams)
793794

794795
if not has_admin_permission:
795796
logger.warning(f"Admin permission denied: user={user_context['email']}")
@@ -1012,14 +1013,15 @@ async def has_admin_permission(self) -> bool:
10121013
Returns:
10131014
bool: True if user has admin permissions
10141015
"""
1016+
token_teams = self.user_context.get("token_teams")
10151017
if self.db_session:
10161018
# Use existing session
10171019
permission_service = PermissionService(self.db_session)
1018-
return await permission_service.check_admin_permission(self.user_context["email"])
1020+
return await permission_service.check_admin_permission(self.user_context["email"], token_teams=token_teams)
10191021
# Create fresh db session
10201022
with fresh_db_session() as db:
10211023
permission_service = PermissionService(db)
1022-
return await permission_service.check_admin_permission(self.user_context["email"])
1024+
return await permission_service.check_admin_permission(self.user_context["email"], token_teams=token_teams)
10231025

10241026
async def has_any_permission(self, permissions: List[str], resource_type: Optional[str] = None, team_id: Optional[str] = None) -> bool:
10251027
"""Check if user has any of the specified permissions.

mcpgateway/routers/rbac.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ async def get_my_permissions(team_id: Optional[str] = Query(None, description="T
590590
"""
591591
try:
592592
permission_service = PermissionService(db)
593-
permissions = await permission_service.get_user_permissions(user_email=user["email"], team_id=team_id)
593+
permissions = await permission_service.get_user_permissions(user_email=user["email"], team_id=team_id, token_teams=user.get("token_teams"))
594594

595595
result = sorted(list(permissions))
596596
db.commit()

mcpgateway/routers/tokens.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,18 @@ async def _get_caller_permissions(
8686
Returns:
8787
List of permissions, or ["*"] for admins
8888
"""
89-
if current_user.get("is_admin"):
90-
return ["*"] # Admins can grant anything
89+
# SECURITY: Only treat admin as unrestricted when token is un-narrowed.
90+
# Narrowed or public-only admin sessions must derive permissions through
91+
# the token-aware path to enforce Layer 1 scope containment.
92+
token_teams = current_user.get("token_teams")
93+
if current_user.get("is_admin") and token_teams is None:
94+
return ["*"] # Un-narrowed admins can grant anything
9195

9296
permission_service = PermissionService(db)
9397
permissions = await permission_service.get_user_permissions(
9498
user_email=current_user["email"],
9599
team_id=team_id,
100+
token_teams=token_teams, # SECURITY: Respect token narrowing
96101
)
97102
return list(permissions) if permissions else None
98103

@@ -130,8 +135,12 @@ async def create_token(
130135
# Multi-team users must specify team_id explicitly to avoid ambiguity.
131136
# Admins with teams=null are exempt and may still create global-scope tokens.
132137
effective_team_id = request.team_id
133-
if effective_team_id is None and not current_user.get("is_admin"):
134-
user_teams = current_user.get("token_teams") or []
138+
caller_token_teams = current_user.get("token_teams")
139+
# Only un-narrowed admins (token_teams=None) are exempt from auto-inheritance.
140+
# Narrowed admin sessions use the same team-scoping logic as non-admins.
141+
is_unrestricted_admin = current_user.get("is_admin") and caller_token_teams is None
142+
if effective_team_id is None and not is_unrestricted_admin:
143+
user_teams = caller_token_teams or []
135144
if len(user_teams) == 1:
136145
effective_team_id = user_teams[0]
137146
logger.debug("Auto-inherited team_id=%s for token creation by %s", effective_team_id, current_user["email"])
@@ -534,7 +543,10 @@ async def list_all_tokens(
534543
"""
535544
_require_authenticated_session(current_user)
536545

537-
if not current_user["is_admin"]:
546+
# SECURITY: Require un-narrowed admin. Narrowed/public-only admin sessions
547+
# must not access the token oversight surface to prevent privilege escalation.
548+
token_teams = current_user.get("token_teams")
549+
if not current_user.get("is_admin") or token_teams is not None:
538550
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
539551

540552
service = TokenCatalogService(db)
@@ -618,7 +630,10 @@ async def admin_revoke_token(
618630
"""
619631
_require_authenticated_session(current_user)
620632

621-
if not current_user["is_admin"]:
633+
# SECURITY: Require un-narrowed admin. Narrowed/public-only admin sessions
634+
# must not revoke arbitrary tokens to prevent privilege escalation.
635+
revoke_token_teams = current_user.get("token_teams")
636+
if not current_user.get("is_admin") or revoke_token_teams is not None:
622637
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
623638

624639
service = TokenCatalogService(db)

0 commit comments

Comments
 (0)