- working central server auth - added ci/cd
This commit is contained in:
4
.env.example
Executable file
4
.env.example
Executable file
@@ -0,0 +1,4 @@
|
||||
RABBITMQ_DEFAULT_USER=backend_admin
|
||||
RABBITMQ_DEFAULT_PASS=your_strong_password
|
||||
|
||||
BACKEND_URL=https://quantum-backend.example.com
|
||||
54
.gitea/workflows/build-and-push.yaml
Normal file
54
.gitea/workflows/build-and-push.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Build and Deploy Docker Image
|
||||
|
||||
# Controls when the workflow will run. Here, it runs on every push to the 'main' branch.
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
# Environment variables used across the workflow
|
||||
env:
|
||||
# The URL of your Gitea instance (without http:// or https://)
|
||||
GITEA_INSTANCE_URL: git.deowl.ru
|
||||
# The full name of your image (e.g., 'myusername/myproject')
|
||||
IMAGE_NAME: vkrb/rabbitmq-auth-backend
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
# Runs the job on a runner with the 'ubuntu-latest' label.
|
||||
runs-on: ubuntu-latest
|
||||
# Optional but recommended: specifies the container image to use for the job.
|
||||
# This ensures a consistent environment with Docker tools pre-installed.
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
|
||||
steps:
|
||||
# 1. Check out your repository code so the workflow can access it.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 2. Set up Docker Buildx, which is needed for building images.
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# 3. Log in to your Gitea instance's Container Registry.
|
||||
# It uses secrets you must define in your repository settings.
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITEA_INSTANCE_URL }}
|
||||
username: ${{ gitea.repository_owner }}
|
||||
# Use a secret for the password/token. See Step 3 for setup.
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
# 4. Build the Docker image from your 'dockerfile_build' directory
|
||||
# and push it to the Gitea registry.
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
# The path to the directory containing your Dockerfile
|
||||
context: ./dockerfile_build
|
||||
push: true
|
||||
# Tag the image with the Gitea instance, image name, and the git commit SHA.
|
||||
tags: |
|
||||
${{ env.GITEA_INSTANCE_URL }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.GITEA_INSTANCE_URL }}/${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
data
|
||||
.env
|
||||
@@ -1,3 +1 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
python-multipart
|
||||
fastapi[all]==0.121.3
|
||||
|
||||
@@ -1,46 +1,279 @@
|
||||
from fastapi import FastAPI, Request, Form
|
||||
from fastapi.responses import PlainTextResponse
|
||||
# rabbitmq_auth_backend.py
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Dict, List, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
app = FastAPI()
|
||||
from fastapi import FastAPI, Form, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import PlainTextResponse
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="RabbitMQ HTTP Auth Backend")
|
||||
|
||||
|
||||
# Configuration
|
||||
YOUR_API_BASE_URL = os.environ["BACKEND_URL"]
|
||||
|
||||
|
||||
def get_default_headers(token: str = None, url: str = None) -> Dict[str, str]:
|
||||
"""
|
||||
Get default headers for API requests.
|
||||
"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Connection": "keep-alive",
|
||||
}
|
||||
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
if url:
|
||||
headers["Referer"] = url
|
||||
# Extract host from URL
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed_url = urlparse(url)
|
||||
headers["Host"] = parsed_url.netloc
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
async def validate_token_and_get_user(token: str) -> dict | None:
|
||||
"""
|
||||
Validate the Keycloak token and get user info.
|
||||
This replicates what your get_current_token_payload does.
|
||||
"""
|
||||
try:
|
||||
url = f"{YOUR_API_BASE_URL}/user"
|
||||
headers = get_default_headers(token=token, url=url)
|
||||
|
||||
# Call your API's /me endpoint to validate token
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers=headers,
|
||||
method="GET",
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
data = json.loads(response.read().decode())
|
||||
return data
|
||||
else:
|
||||
logger.error(f"Token validation failed: {response.status}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating token: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def check_system_access(system_id: int, user_info: dict) -> bool:
|
||||
"""
|
||||
Check if the user has access to the computational system.
|
||||
Uses your existing API endpoints.
|
||||
"""
|
||||
try:
|
||||
params = urlencode({"system_id": system_id})
|
||||
url = f"{YOUR_API_BASE_URL}/machine/system?{params}"
|
||||
headers = get_default_headers(token=user_info.get("token"), url=url)
|
||||
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers=headers,
|
||||
method="GET",
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
return True
|
||||
elif response.status == 403:
|
||||
logger.warning(
|
||||
f"User {user_info.get('keycloak_id')} denied access to system {system_id}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
logger.error(f"Error checking system access: {response.status}")
|
||||
return False
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 403:
|
||||
logger.warning(
|
||||
f"User {user_info.get('keycloak_id')} denied access to system {system_id}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
logger.error(f"Error checking system access: {e.code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking system access: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def get_system_teams(system_id: int) -> List[Dict]:
|
||||
"""
|
||||
Get all teams that this system is a member of.
|
||||
"""
|
||||
try:
|
||||
url = f"{YOUR_API_BASE_URL}/machine/system/team?system_id={system_id}"
|
||||
headers = get_default_headers(url=url)
|
||||
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers=headers,
|
||||
method="GET",
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
data = json.loads(response.read().decode())
|
||||
return data
|
||||
else:
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting system teams: {e}")
|
||||
return []
|
||||
|
||||
USERS = {
|
||||
"admin": {"password": "secret", "tags": ["administrator", "management"]},
|
||||
"user1": {"password": "password123", "tags": ["management"]},
|
||||
}
|
||||
|
||||
@app.post("/rabbit/auth/user")
|
||||
async def auth_user(username: str = Form(...), password: str = Form(...)):
|
||||
user = USERS.get(username)
|
||||
if user and user["password"] == password:
|
||||
return PlainTextResponse("allow " + ", ".join(user["tags"]))
|
||||
return PlainTextResponse("deny", status_code=403)
|
||||
async def authenticate_user(
|
||||
username: str = Form(...), # Changed from AuthRequest to form parameters
|
||||
password: str = Form(...),
|
||||
):
|
||||
"""
|
||||
RabbitMQ calls this to authenticate a user.
|
||||
Username format: "system_{system_id}"
|
||||
Password: Keycloak token
|
||||
"""
|
||||
logger.info(f"Auth request for username: {username}")
|
||||
|
||||
try:
|
||||
system_id = int(username)
|
||||
except (IndexError, ValueError):
|
||||
logger.warning(f"Invalid system_id in username: {username}")
|
||||
return PlainTextResponse("deny")
|
||||
|
||||
# Validate the token and get user info
|
||||
user_info = await validate_token_and_get_user(password)
|
||||
if not user_info:
|
||||
logger.warning(f"Invalid token for system {system_id}")
|
||||
return PlainTextResponse("deny")
|
||||
|
||||
# Store token in user_info for subsequent checks
|
||||
user_info["token"] = password
|
||||
|
||||
# Check if user has access to the system
|
||||
if not await check_system_access(system_id, user_info):
|
||||
return PlainTextResponse("deny")
|
||||
logger.info(
|
||||
f"Authentication successful for system {system_id}, user {user_info.get('keycloak_id')}"
|
||||
)
|
||||
|
||||
# Return success - RabbitMQ will allow connection
|
||||
return PlainTextResponse("allow")
|
||||
|
||||
|
||||
@app.post("/rabbit/auth/vhost")
|
||||
async def auth_vhost(username: str = Form(...), vhost: str = Form(...), ip: str = Form(...)):
|
||||
if username in USERS:
|
||||
return PlainTextResponse("allow")
|
||||
return PlainTextResponse("deny", status_code=403)
|
||||
async def authorize_vhost(
|
||||
username: str = Form(...), vhost: str = Form(...), ip: Optional[str] = Form(None)
|
||||
):
|
||||
"""
|
||||
RabbitMQ calls this to check vhost permissions.
|
||||
Only allow specific vhosts.
|
||||
"""
|
||||
logger.info(f"VHost check for user: {username}, vhost: {vhost}")
|
||||
|
||||
# Only allow specific vhosts
|
||||
allowed_vhosts = ["/"]
|
||||
|
||||
if vhost not in allowed_vhosts:
|
||||
logger.warning(f"Unauthorized vhost access attempt: {vhost}")
|
||||
return PlainTextResponse("deny")
|
||||
|
||||
logger.info(f"VHost access granted for {username} to {vhost}")
|
||||
return PlainTextResponse("allow")
|
||||
|
||||
|
||||
@app.post("/rabbit/auth/resource")
|
||||
async def auth_resource(username: str = Form(...), vhost: str = Form(...), resource: str = Form(...), name: str = Form(...), permission: str = Form(...)):
|
||||
if username == "admin":
|
||||
return PlainTextResponse("allow")
|
||||
async def authorize_resource(
|
||||
username: str = Form(...),
|
||||
vhost: str = Form(...),
|
||||
resource: str = Form(...),
|
||||
name: str = Form(...),
|
||||
permission: str = Form(...),
|
||||
ip: Optional[str] = Form(None),
|
||||
):
|
||||
"""
|
||||
RabbitMQ calls this to check resource permissions.
|
||||
"""
|
||||
logger.info(
|
||||
f"Resource check for {username} on vhost {vhost}, "
|
||||
f"resource: {resource}, name: {name}, "
|
||||
f"permission: {permission}"
|
||||
)
|
||||
|
||||
if username == "user1" and resource == "queue" and name.startswith("public_"):
|
||||
if permission in ["read", "configure"]:
|
||||
return PlainTextResponse("allow")
|
||||
try:
|
||||
system_id = int(username)
|
||||
except (IndexError, ValueError):
|
||||
return PlainTextResponse("deny")
|
||||
|
||||
return PlainTextResponse("deny", status_code=403)
|
||||
# Permission logic based on vhost and resource type
|
||||
if vhost == "/":
|
||||
# On heartbeat vhost, allow publishing to "heartbeat" exchange
|
||||
if resource == "exchange":
|
||||
if (name == "heartbeat" or name == "progress_report") and (
|
||||
permission == "write" or permission == "configure"
|
||||
):
|
||||
logger.info(f"Publish permission granted for heartbeat exchange")
|
||||
return PlainTextResponse("allow")
|
||||
else:
|
||||
logger.warning(f"Unauthorized exchange access: {name}")
|
||||
|
||||
@app.post("/rabbit/auth/topic")
|
||||
async def auth_topic(username: str = Form(...),
|
||||
vhost: str = Form(...),
|
||||
resource: str = Form(...),
|
||||
name: str = Form(...),
|
||||
permission: str = Form(...),
|
||||
topic_path: str = Form(...),
|
||||
):
|
||||
# Handle queue operations on team vhost
|
||||
if resource == "queue":
|
||||
# Pattern: team_{team_id}.qubits_{N}
|
||||
team_queue_pattern = r"^team_(\w+)\.qubits_(\d+)$"
|
||||
match = re.match(team_queue_pattern, name)
|
||||
|
||||
if username == "admin" or (username == "user1" and routing_key.startswith("logs.")):
|
||||
return PlainTextResponse("allow")
|
||||
return PlainTextResponse("deny", status_code=403)
|
||||
if not match:
|
||||
return PlainTextResponse("deny")
|
||||
|
||||
team_id = match.group(1)
|
||||
qubits = int(match.group(2))
|
||||
|
||||
# Get teams this system belongs to
|
||||
system_teams = await get_system_teams(system_id)
|
||||
logger.info(system_teams)
|
||||
|
||||
team = list(
|
||||
filter(lambda x: int(x.get("team_id")) == int(team_id), system_teams)
|
||||
)
|
||||
|
||||
# Check if system is a member of this team
|
||||
if len(team) == 0:
|
||||
logger.warning(f"System {system_id} not a member of team {team_id}")
|
||||
return PlainTextResponse("deny")
|
||||
team = team[0]
|
||||
|
||||
if team.get("qubits_given") < qubits:
|
||||
logger.warning(f"System {system_id} does not have that many qubits")
|
||||
return PlainTextResponse("deny")
|
||||
# Allow configure and read permissions on team qubit queues
|
||||
if permission in ["configure", "read"]:
|
||||
logger.info(f"System {system_id} granted {permission} on queue {name}")
|
||||
return PlainTextResponse("allow")
|
||||
|
||||
return PlainTextResponse("deny")
|
||||
|
||||
# Default deny
|
||||
return PlainTextResponse("deny")
|
||||
|
||||
@@ -4,12 +4,13 @@ listeners.tcp.default = 5672
|
||||
auth_backends.1 = http
|
||||
auth_backends.2 = internal
|
||||
|
||||
|
||||
auth_http.http_method = post
|
||||
auth_http.user_path = http://rabbit-auth-server:8080/rabbit/auth/user
|
||||
auth_http.vhost_path = http://rabbit-auth-server:8080/rabbit/auth/vhost
|
||||
auth_http.resource_path = http://rabbit-auth-server:8080/rabbit/auth/resource
|
||||
auth_http.topic_path = http://rabbit-auth-server:8080/rabbit/auth/topic
|
||||
|
||||
|
||||
# Optional: timeout settings (milliseconds)
|
||||
auth_http.request_timeout = 5000
|
||||
auth_http.connection_timeout = 3000
|
||||
auth_http.connection_timeout = 3000
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
image: rabbitmq:3-management
|
||||
container_name: rabbitmq
|
||||
restart: unless-stopped
|
||||
env_file: "rabbitmq.env"
|
||||
env_file: .env
|
||||
depends_on:
|
||||
- rabbit-auth-server
|
||||
volumes:
|
||||
@@ -15,8 +15,9 @@ services:
|
||||
- 5672:5672/tcp
|
||||
|
||||
rabbit-auth-server:
|
||||
build: ./auth_backend
|
||||
image: git.deowl.ru/vkrb/rabbitmq-auth-backend:0.1.0
|
||||
container_name: rabbit-auth-server
|
||||
env_file: .env
|
||||
|
||||
volumes:
|
||||
lib:
|
||||
|
||||
Reference in New Issue
Block a user