-
-
Notifications
You must be signed in to change notification settings - Fork 401
fix: require admin auth on governance and coalition GET endpoints #6321
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
base: main
Are you sure you want to change the base?
Changes from all commits
69a919a
bc9ff05
d19f7a0
db2cf6f
751f49c
e90a0f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| """ | ||
| import json | ||
| import html | ||
| import hmac | ||
| import math | ||
| import os | ||
| import time | ||
|
|
@@ -277,6 +278,13 @@ def _authenticate_contract_agent(db, allowed_agents, body_bytes): | |
| @beacon_api.route('/api/agents', methods=['GET']) | ||
| def get_agents(): | ||
| """Get all registered agents.""" | ||
| # SECURITY: Require admin key — exposes all relay agents with pubkeys, coinbase addresses, status | ||
| admin_key = os.environ.get("RC_ADMIN_KEY", "") | ||
| if not admin_key: | ||
| return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 | ||
| provided_key = request.headers.get("X-Admin-Key", "") | ||
| if not hmac.compare_digest(provided_key, admin_key): | ||
| return jsonify({'error': 'Unauthorized'}), 401 | ||
| try: | ||
| db = get_db() | ||
| rows = db.execute( | ||
|
|
@@ -302,6 +310,13 @@ def get_agents(): | |
| @beacon_api.route('/api/agent/<agent_id>', methods=['GET']) | ||
| def get_agent(agent_id): | ||
| """Get single agent details.""" | ||
| # SECURITY: Require admin key — exposes agent pubkey, coinbase address, status | ||
| admin_key = os.environ.get("RC_ADMIN_KEY", "") | ||
| if not admin_key: | ||
| return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 | ||
| provided_key = request.headers.get("X-Admin-Key", "") | ||
| if not hmac.compare_digest(provided_key, admin_key): | ||
| return jsonify({'error': 'Unauthorized'}), 401 | ||
| try: | ||
| db = get_db() | ||
| row = db.execute( | ||
|
|
@@ -537,6 +552,13 @@ def beacon_atlas(): | |
| @beacon_api.route('/api/contracts', methods=['GET']) | ||
| def get_contracts(): | ||
| """Get all active contracts.""" | ||
| # SECURITY: Require admin key — exposes all beacon contracts, agent IDs, contract terms | ||
| admin_key = os.environ.get("RC_ADMIN_KEY", "") | ||
| if not admin_key: | ||
| return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 | ||
| provided_key = request.headers.get("X-Admin-Key", "") | ||
| if not hmac.compare_digest(provided_key, admin_key): | ||
| return jsonify({'error': 'Unauthorized'}), 401 | ||
| try: | ||
| db = get_db() | ||
| rows = db.execute( | ||
|
|
@@ -731,6 +753,13 @@ def update_contract(contract_id): | |
| @beacon_api.route('/api/bounties', methods=['GET']) | ||
| def get_bounties(): | ||
| """Get all active bounties (from cache or DB).""" | ||
| # SECURITY: Require admin key — exposes all beacon bounties with reward amounts and agent info | ||
| admin_key = os.environ.get("RC_ADMIN_KEY", "") | ||
| if not admin_key: | ||
| return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 | ||
| provided_key = request.headers.get("X-Admin-Key", "") | ||
| if not hmac.compare_digest(provided_key, admin_key): | ||
| return jsonify({'error': 'Unauthorized'}), 401 | ||
| try: | ||
| db = get_db() | ||
| rows = db.execute( | ||
|
|
@@ -983,6 +1012,13 @@ def complete_bounty(bounty_id): | |
| @beacon_api.route('/api/reputation', methods=['GET']) | ||
| def get_reputation(): | ||
| """Get all agent reputations.""" | ||
| # SECURITY: Require admin key — exposes all agent scores, RTC earnings, breach history | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue for reputation reads: |
||
| admin_key = os.environ.get("RC_ADMIN_KEY", "") | ||
| if not admin_key: | ||
| return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 | ||
| provided_key = request.headers.get("X-Admin-Key", "") | ||
| if not hmac.compare_digest(provided_key, admin_key): | ||
| return jsonify({'error': 'Unauthorized'}), 401 | ||
| try: | ||
| db = get_db() | ||
| rows = db.execute("SELECT * FROM beacon_reputation ORDER BY score DESC").fetchall() | ||
|
|
@@ -1006,6 +1042,13 @@ def get_reputation(): | |
| @beacon_api.route('/api/reputation/<agent_id>', methods=['GET']) | ||
| def get_agent_reputation(agent_id): | ||
| """Get single agent reputation.""" | ||
| # SECURITY: Require admin key — exposes agent score, RTC earnings, breach count | ||
| admin_key = os.environ.get("RC_ADMIN_KEY", "") | ||
| if not admin_key: | ||
| return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 | ||
| provided_key = request.headers.get("X-Admin-Key", "") | ||
| if not hmac.compare_digest(provided_key, admin_key): | ||
| return jsonify({'error': 'Unauthorized'}), 401 | ||
| try: | ||
| db = get_db() | ||
| row = db.execute("SELECT * FROM beacon_reputation WHERE agent_id = ?", (agent_id,)).fetchone() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,18 @@ | |
|
|
||
| log = logging.getLogger("rip0002_governance") | ||
|
|
||
|
|
||
| def _admin_key_required(): | ||
| """Return 401 if X-Admin-Key header is missing or wrong.""" | ||
| import os | ||
| expected = os.environ.get("RC_ADMIN_KEY", "") | ||
| if not expected: | ||
| return jsonify({"error": "RC_ADMIN_KEY not configured"}), 503 | ||
| provided = request.headers.get("X-Admin-Key", "") | ||
| if not hmac.compare_digest(provided, expected): | ||
| return jsonify({"error": "Unauthorized — admin key required"}), 401 | ||
| return None | ||
|
|
||
| # Signature window: reject requests with timestamps older than this | ||
| _SIGNATURE_MAX_AGE_SECONDS = 300 # 5 minutes | ||
|
|
||
|
|
@@ -414,6 +426,10 @@ def create_proposal(): | |
| # -- GET /api/governance/proposals ---------------------------------------- | ||
| @bp.route("/api/governance/proposals", methods=["GET"]) | ||
| def list_proposals(): | ||
| # SECURITY: Require admin key — exposes all governance proposals, votes, miner activity | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The governance gate may be the intended part of this PR, but the branch contains 14 files of unrelated changes including Beacon Atlas, Hall of Rust, Rewards, Airdrop, Sophia, OTC bridge, setup_miner, glitch API, and monitor CLI. That extra scope is what is failing CI, so I would split this down to the governance/coalition routes and test those specifically. |
||
| err = _admin_key_required() | ||
| if err: | ||
| return err | ||
| _settle_expired_proposals(db_path) | ||
| status_filter = request.args.get("status") | ||
| limit, error_response = _parse_non_negative_int_arg("limit", 50, max_value=200) | ||
|
|
@@ -448,6 +464,10 @@ def list_proposals(): | |
| # -- GET /api/governance/proposal/<n> ------------------------------------ | ||
| @bp.route("/api/governance/proposal/<int:proposal_id>", methods=["GET"]) | ||
| def get_proposal(proposal_id: int): | ||
| # SECURITY: Require admin key — exposes proposal details, votes, voter identities | ||
| err = _admin_key_required() | ||
| if err: | ||
| return err | ||
| _settle_expired_proposals(db_path) | ||
| try: | ||
| with sqlite3.connect(db_path) as conn: | ||
|
|
@@ -593,6 +613,10 @@ def cast_vote(): | |
| # -- GET /api/governance/results/<n> ------------------------------------ | ||
| @bp.route("/api/governance/results/<int:proposal_id>", methods=["GET"]) | ||
| def get_results(proposal_id: int): | ||
| # SECURITY: Require admin key — exposes vote tallies, quorum stats, active miner count | ||
| err = _admin_key_required() | ||
| if err: | ||
| return err | ||
| _settle_expired_proposals(db_path) | ||
| try: | ||
| with sqlite3.connect(db_path) as conn: | ||
|
|
@@ -676,6 +700,10 @@ def founder_veto(proposal_id: int): | |
| # -- GET /api/governance/stats ------------------------------------------ | ||
| @bp.route("/api/governance/stats", methods=["GET"]) | ||
| def governance_stats(): | ||
| # SECURITY: Require admin key — exposes governance participation stats, voter counts | ||
| err = _admin_key_required() | ||
| if err: | ||
| return err | ||
| _settle_expired_proposals(db_path) | ||
| try: | ||
| with sqlite3.connect(db_path) as conn: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This unconditional admin gate breaks the existing contract lifecycle. The workflow signs
POST /api/contractswith Beacon agent auth, then callsGET /api/contractsto list/verify the offered contract; after this change that list call returns 401, sotests/test_beacon_atlas_behavior.py::TestBeaconAtlasAPIBehavior::test_create_contract_workflowfails. If this read endpoint is becoming private, the PR needs to update the contract workflow/callers and tests; otherwise keep this route compatible with the existing signed contract flow.