Skip to content

Commit c9b0bc9

Browse files
Merge pull request #8 from CodeSignal/add-runtime-configuration-option
Add reset endpoints for the auth, todos and users to be reconfigured at runtime
2 parents cc288e2 + 13b915b commit c9b0bc9

File tree

8 files changed

+721
-52
lines changed

8 files changed

+721
-52
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Build and Push Docker Image
2+
run-name: Build and push todo-api image for ${{ github.ref_name }} by @${{ github.actor }}
3+
4+
on:
5+
push:
6+
branches:
7+
- 'main'
8+
- 'develop'
9+
tags:
10+
- 'v*'
11+
pull_request:
12+
branches:
13+
- 'main'
14+
15+
env:
16+
REGISTRY: ghcr.io
17+
IMAGE_NAME: ${{ github.repository }}
18+
19+
permissions:
20+
contents: read
21+
packages: write
22+
23+
jobs:
24+
build-and-push:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@v4
29+
30+
- name: Set up Docker Buildx
31+
uses: docker/setup-buildx-action@v3
32+
33+
- name: Log in to Container Registry
34+
uses: docker/login-action@v3
35+
with:
36+
registry: ${{ env.REGISTRY }}
37+
username: ${{ github.actor }}
38+
password: ${{ secrets.GITHUB_TOKEN }}
39+
40+
- name: Extract metadata
41+
id: meta
42+
uses: docker/metadata-action@v5
43+
with:
44+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
45+
tags: |
46+
# set latest tag for default branch
47+
type=ref,event=branch
48+
type=ref,event=pr
49+
type=semver,pattern={{version}}
50+
type=semver,pattern={{major}}.{{minor}}
51+
type=semver,pattern={{major}}
52+
type=raw,value=latest,enable={{is_default_branch}}
53+
54+
- name: Build and push Docker image
55+
uses: docker/build-push-action@v5
56+
with:
57+
context: .
58+
platforms: linux/amd64,linux/arm64
59+
push: true
60+
tags: ${{ steps.meta.outputs.tags }}
61+
labels: ${{ steps.meta.outputs.labels }}
62+
cache-from: type=gha
63+
cache-to: type=gha,mode=max

app/config/auth_config.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,84 @@ def __init__(self):
1212
self.api_key = None
1313
self.jwt_secret = None
1414
self.session_secret = None
15-
15+
1616
def configure_api_key(self, api_key):
1717
self.auth_method = AuthMethod.API_KEY
1818
self.api_key = api_key
19-
19+
2020
def configure_jwt(self, secret_key):
2121
self.auth_method = AuthMethod.JWT
2222
self.jwt_secret = secret_key
23-
23+
2424
def configure_session(self, secret_key):
2525
self.auth_method = AuthMethod.SESSION
2626
self.session_secret = secret_key
27-
27+
2828
def disable_auth(self):
29-
self.auth_method = AuthMethod.NONE
29+
self.auth_method = AuthMethod.NONE
30+
31+
def update_from_dict(self, config_dict):
32+
"""Update authentication configuration from a dictionary.
33+
34+
Args:
35+
config_dict (dict): Configuration dictionary with 'auth' key containing:
36+
- method: one of 'none', 'api_key', 'jwt', 'session'
37+
- api_key: API key (required if method is 'api_key')
38+
- secret: Secret key (required if method is 'jwt' or 'session')
39+
40+
Raises:
41+
ValueError: If configuration is invalid
42+
"""
43+
auth_config = config_dict.get('auth', {})
44+
method = auth_config.get('method', 'none')
45+
46+
# Validate method
47+
valid_methods = [e.value for e in AuthMethod]
48+
if method not in valid_methods:
49+
raise ValueError(f"Invalid authentication method: {method}. Must be one of: {valid_methods}")
50+
51+
# Reset current configuration
52+
self.auth_method = AuthMethod.NONE
53+
self.api_key = None
54+
self.jwt_secret = None
55+
self.session_secret = None
56+
57+
# Apply new configuration
58+
if method == 'none':
59+
self.disable_auth()
60+
elif method == 'api_key':
61+
api_key = auth_config.get('api_key')
62+
if not api_key:
63+
raise ValueError("API key must be provided when using api_key authentication")
64+
self.configure_api_key(api_key)
65+
elif method == 'jwt':
66+
secret = auth_config.get('secret')
67+
if not secret:
68+
raise ValueError("Secret key must be provided when using JWT authentication")
69+
self.configure_jwt(secret)
70+
elif method == 'session':
71+
secret = auth_config.get('secret')
72+
if not secret:
73+
raise ValueError("Secret key must be provided when using session authentication")
74+
self.configure_session(secret)
75+
76+
def to_dict(self):
77+
"""Convert current configuration to dictionary format.
78+
79+
Returns:
80+
dict: Configuration dictionary in the same format as the YAML file
81+
"""
82+
config = {
83+
'auth': {
84+
'method': self.auth_method.value
85+
}
86+
}
87+
88+
if self.auth_method == AuthMethod.API_KEY and self.api_key:
89+
config['auth']['api_key'] = self.api_key
90+
elif self.auth_method == AuthMethod.JWT and self.jwt_secret:
91+
config['auth']['secret'] = self.jwt_secret
92+
elif self.auth_method == AuthMethod.SESSION and self.session_secret:
93+
config['auth']['secret'] = self.session_secret
94+
95+
return config

app/main.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from routes.docs import docs_bp
55
from routes.notes import notes_bp
66
from routes.auth import auth_bp, init_auth_routes
7-
from middleware.auth_middleware import AuthMiddleware
7+
from middleware.auth_middleware import AuthMiddleware, set_auth_middleware_instance
88
from utils.config import load_config, load_initial_todos, load_initial_users
99
from utils.auth import setup_auth_config
1010
from services.auth_service import init_auth_service, add_user
@@ -13,27 +13,27 @@
1313

1414
def create_app(auth_config):
1515
"""Create and configure the Flask application.
16-
16+
1717
This function:
1818
1. Creates a new Flask instance
1919
2. Configures app settings and secrets
2020
3. Sets up authentication middleware
2121
4. Registers blueprints with their URL prefixes
22-
22+
2323
Args:
2424
auth_config: Authentication configuration object
25-
25+
2626
Returns:
2727
Flask: Configured Flask application instance
2828
"""
2929
app = Flask(__name__)
30-
30+
3131
# Configure application settings
3232
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True
3333
app.config['SECRET_KEY'] = secrets.token_hex(32) # Generate secure random secret key
3434
app.config['auth_config'] = auth_config
3535
app.config['initial_todos'] = load_initial_todos() # Load initial todos from config file
36-
36+
3737
# Configure Swagger
3838
template = {
3939
"swagger": "2.0",
@@ -43,26 +43,29 @@ def create_app(auth_config):
4343
"version": "1.0.0"
4444
}
4545
}
46-
46+
4747
app.config['SWAGGER'] = {
4848
'title': 'Todo API',
4949
'uiversion': 3,
5050
'specs_route': '/',
5151
'url_prefix': '/swagger'
5252
}
53-
53+
5454
Swagger(app, template=template)
55-
55+
5656
# Set up authentication
5757
init_auth_routes(auth_config)
5858
auth_middleware = AuthMiddleware(auth_config)
59-
59+
60+
# Store the middleware instance globally for runtime updates
61+
set_auth_middleware_instance(auth_middleware)
62+
6063
# Define routes that require authentication
6164
protected_blueprints = {
6265
todos_bp: "/todos", # Todo management endpoints
6366
notes_bp: "/notes" # Note management endpoints
6467
}
65-
68+
6669
# Register protected routes with authentication middleware
6770
for blueprint, url_prefix in protected_blueprints.items():
6871
auth_middleware.protect_blueprint(blueprint)
@@ -84,17 +87,17 @@ def seed_users():
8487
try:
8588
# Load authentication configuration from config file
8689
auth_method, secret = load_config()
87-
90+
8891
# Set up authentication based on configuration
8992
auth_config = setup_auth_config(auth_method, secret)
9093
seed_users()
91-
94+
9295
# Initialize auth service
9396
init_auth_service(auth_config)
94-
97+
9598
# Create and configure the application
9699
app = create_app(auth_config)
97-
100+
98101
# Start the server
99102
app.run(host="0.0.0.0", port=8000) # Listen on all interfaces, port 8000
100103
except ValueError as e:

app/middleware/auth_middleware.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,38 @@ def _validate_session(self):
5454
"""Validate session authentication"""
5555
if not session.get("authenticated"):
5656
return jsonify({"error": "Valid session required"}), 401
57-
57+
5858
# Check if session has been invalidated
5959
current_session = request.cookies.get('session')
6060
if current_session and current_session in invalidated_sessions:
6161
session.clear()
6262
return jsonify({"error": "Session has been invalidated"}), 401
63-
64-
return None
63+
64+
return None
65+
66+
# Global middleware instance for runtime updates
67+
_global_middleware_instance = None
68+
69+
def reset_auth_middleware(new_config: AuthConfig):
70+
"""Reset the global auth middleware instance with new configuration.
71+
72+
This function updates the middleware configuration that's used
73+
by all protected blueprints. Due to Flask's blueprint registration
74+
mechanics, we update the global instance that blueprints reference.
75+
76+
Args:
77+
new_config: New AuthConfig instance
78+
"""
79+
global _global_middleware_instance
80+
if _global_middleware_instance:
81+
_global_middleware_instance.config = new_config
82+
print(f"Auth middleware reset with new configuration: {new_config.auth_method.value}")
83+
84+
def get_auth_middleware_instance():
85+
"""Get the global middleware instance."""
86+
return _global_middleware_instance
87+
88+
def set_auth_middleware_instance(instance):
89+
"""Set the global middleware instance."""
90+
global _global_middleware_instance
91+
_global_middleware_instance = instance

0 commit comments

Comments
 (0)