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

Restricted Queues #406

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
10 changes: 7 additions & 3 deletions cli/testflinger_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,14 @@ def submit(self):
except FileNotFoundError:
sys.exit(f"File not found: {self.args.filename}")
job_dict = yaml.safe_load(data)
if "job_priority" in job_dict:
jwt = self.authenticate_with_server()
jwt = self.authenticate_with_server()
if jwt is not None:
auth_headers = {"Authorization": jwt}
else:
if "job_priority" in job_dict:
sys.exit(
"Must provide client id and secret key for priority jobs"
)
auth_headers = None

attachments_data = self.extract_attachment_data(job_dict)
Expand Down Expand Up @@ -541,7 +545,7 @@ def authenticate_with_server(self):
and return JWT with permissions
"""
if self.client_id is None or self.secret_key is None:
sys.exit("Must provide client id and secret key for priority jobs")
return None

try:
jwt = self.client.authenticate(self.client_id, self.secret_key)
Expand Down
5 changes: 3 additions & 2 deletions docs/explanation/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ Authentication and Authorisation

Authentication requires a client_id and a secret_key. These credentials can be
obtained by contacting the server administrator with the queues you want priority
access for as well as the maximum priority level to set for each queue. The
expectation is that these credentials are shared between users on a team.
access for, the maximum priority level to set for each queue, and any restricted
queues that you need access to. The expectation is that these credentials are
shared between users on a team.

These credentials can be :doc:`set using the Testflinger CLI <../how-to/authentication>`.
1 change: 1 addition & 0 deletions docs/explanation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ This section covers conceptual questions about Testflinger.
agents
queues
job-priority
restricted-queues
authentication
2 changes: 1 addition & 1 deletion docs/how-to/authentication.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Authentication using Testflinger CLI
====================================

:doc:`Authentication <../explanation/authentication>` is only required for submitting jobs with priority.
:doc:`Authentication <../explanation/authentication>` is only required for submitting jobs with priority or submitting jobs to a restricted queue.

Authenticating with Testflinger server requires a client id and a secret key.
These credentials can be provided to the CLI using the environment variables
Expand Down
60 changes: 33 additions & 27 deletions server/src/api/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,17 @@ def has_attachments(data: dict) -> bool:
)


def check_token_priority_permission(
def check_token_permissions(
auth_token: str, secret_key: str, priority: int, queue: str
) -> bool:
"""
Validates token received from client and checks if it can
push a job to the queue with the requested priority
"""
is_queue_restricted = database.check_queue_restricted(queue)
if not is_queue_restricted and priority == 0:
return True

if auth_token is None:
abort(401, "Unauthorized")
try:
Expand All @@ -125,7 +129,10 @@ def check_token_priority_permission(
star_priority = max_priority_dict.get("*", 0)
queue_priority = max_priority_dict.get(queue, 0)
max_priority = max(star_priority, queue_priority)
return max_priority >= priority
allowed_queues = decoded_jwt.get("allowed_queues", [])
return max_priority >= priority and (
not is_queue_restricted or queue in allowed_queues
)


def job_builder(data: dict, auth_token: str):
Expand All @@ -151,27 +158,25 @@ def job_builder(data: dict, auth_token: str):
if has_attachments(data):
data["attachments_status"] = "waiting"

if "job_priority" in data:
priority_level = data["job_priority"]
job_queue = data["job_queue"]
allowed = check_token_priority_permission(
auth_token,
os.environ.get("JWT_SIGNING_KEY"),
priority_level,
job_queue,
priority_level = data.get("job_priority", 0)
job_queue = data["job_queue"]
allowed = check_token_permissions(
auth_token,
os.environ.get("JWT_SIGNING_KEY"),
priority_level,
job_queue,
)
if not allowed:
abort(
403,
(
f"Not enough permissions to push to {job_queue}",
f"with priority {priority_level}",
),
)
if not allowed:
abort(
403,
(
f"Not enough permissions to push to {job_queue}",
f"with priority {priority_level}",
),
)
job["job_priority"] = priority_level
data.pop("job_priority")
else:
job["job_priority"] = 0
job["job_priority"] = priority_level
data.pop("job_priority", None)

job["job_id"] = job_id
job["job_data"] = data
return job
Expand Down Expand Up @@ -707,16 +712,18 @@ def queue_wait_time_percentiles_get():
return queue_percentile_data


def generate_token(max_priority, secret_key):
def generate_token(allowed_resources, secret_key):
"""Generates JWT token with queue permission given a secret key"""
expiration_time = datetime.utcnow() + timedelta(seconds=2)
token_payload = {
"exp": expiration_time,
"iat": datetime.now(timezone.utc), # Issued at time
"sub": "access_token",
"max_priority": max_priority,
}

if "max_priority" in allowed_resources:
token_payload["max_priority"] = allowed_resources["max_priority"]
if "allowed_queues" in allowed_resources:
token_payload["allowed_queues"] = allowed_resources["allowed_queues"]
token = jwt.encode(token_payload, secret_key, algorithm="HS256")
return token

Expand All @@ -739,8 +746,7 @@ def validate_client_key_pair(client_id: str, client_key: str):
client_permissions_entry["client_secret_hash"].encode("utf8"),
):
return None
max_priority = client_permissions_entry["max_priority"]
return max_priority
return client_permissions_entry


@v1.post("/oauth2/token")
Expand Down
8 changes: 8 additions & 0 deletions server/src/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,11 @@ def get_provision_log(
if provision_log_entries
else []
)


def check_queue_restricted(queue: str) -> bool:
"""Checks if queue is restricted"""
queue_count = mongo.db.restricted_queues.count_documents(
{"queue_name": queue}
)
return queue_count != 0
8 changes: 8 additions & 0 deletions server/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,19 @@ def mongo_app_with_permissions(mongo_app):
"myqueue": 100,
"myqueue2": 200,
}
allowed_queues = ["rqueue1", "rqueue2"]
mongo.client_permissions.insert_one(
{
"client_id": client_id,
"client_secret_hash": client_key_hash,
"max_priority": max_priority,
"allowed_queues": allowed_queues,
}
)
restricted_queues = [
{"queue_name": "rqueue1"},
{"queue_name": "rqueue2"},
{"queue_name": "rqueue3"},
]
mongo.restricted_queues.insert_many(restricted_queues)
yield app, mongo, client_id, client_key, max_priority
Loading