Skip to content

Commit d1e6fcd

Browse files
committed
feat(api): add authentication requirements to OpenAPI spec
1 parent 00cd555 commit d1e6fcd

27 files changed

Lines changed: 263 additions & 76 deletions

dev/docker/secutils-e2e.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ timeout = 60000
1919

2020
[security]
2121
secrets_encryption_key = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
22-
operators = ["@kratos", "@secutils", "@retrack"]
22+
operators = ["@kratos", "@secutils", "@retrack", "demo@secutils.dev"]
2323

2424
[security.preconfigured_users]
2525
"e2e@secutils.dev" = { handle = "u", tier = "ultimate" }

src/server/handlers.rs

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,28 @@ use crate::{
4141
use actix_web::web;
4242
use utoipa::OpenApi;
4343

44+
struct SecurityAddon;
45+
46+
impl utoipa::Modify for SecurityAddon {
47+
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
48+
if let Some(components) = openapi.components.as_mut() {
49+
components.add_security_scheme(
50+
"bearerAuth",
51+
utoipa::openapi::security::SecurityScheme::Http(
52+
utoipa::openapi::security::HttpBuilder::new()
53+
.scheme(utoipa::openapi::security::HttpAuthScheme::Bearer)
54+
.bearer_format("JWT")
55+
.description(Some(
56+
"JWT token obtained from the authentication service. \
57+
Pass as `Authorization: Bearer <token>`.",
58+
))
59+
.build(),
60+
),
61+
);
62+
}
63+
}
64+
}
65+
4466
/// Resolves the effective user for a shared-resource-aware handler.
4567
///
4668
/// If a `UserShare` is present and its resource matches the expected shared resource, the share
@@ -199,6 +221,10 @@ pub(crate) async fn resolve_shared_user(
199221
api_trackers::api_trackers_test,
200222
api_trackers::api_trackers_debug,
201223
),
224+
modifiers(&SecurityAddon),
225+
security(
226+
("bearerAuth" = [])
227+
),
202228
components(schemas(
203229
// Tags
204230
crate::users::UserTag,
@@ -562,6 +588,9 @@ mod tests {
562588
}
563589
}
564590
}
591+
},
592+
"401": {
593+
"description": "Missing or invalid authentication credentials."
565594
}
566595
}
567596
},
@@ -594,6 +623,9 @@ mod tests {
594623
},
595624
"400": {
596625
"description": "Invalid tag parameters."
626+
},
627+
"401": {
628+
"description": "Missing or invalid authentication credentials."
597629
}
598630
}
599631
}
@@ -714,7 +746,10 @@ mod tests {
714746
}
715747
}
716748
}
717-
}
749+
},
750+
"security": [
751+
{}
752+
]
718753
},
719754
"post": {
720755
"tags": [
@@ -736,6 +771,12 @@ mod tests {
736771
"204": {
737772
"description": "Status was successfully updated."
738773
},
774+
"401": {
775+
"description": "Missing or invalid authentication credentials."
776+
},
777+
"403": {
778+
"description": "Caller is not an operator."
779+
},
739780
"500": {
740781
"description": "Failed to update status."
741782
}
@@ -770,6 +811,9 @@ mod tests {
770811
}
771812
}
772813
}
814+
},
815+
"401": {
816+
"description": "Missing or invalid authentication credentials."
773817
}
774818
}
775819
},
@@ -802,6 +846,9 @@ mod tests {
802846
},
803847
"400": {
804848
"description": "Invalid template parameters."
849+
},
850+
"401": {
851+
"description": "Missing or invalid authentication credentials."
805852
}
806853
}
807854
}
@@ -1093,6 +1140,58 @@ mod tests {
10931140
"###);
10941141
}
10951142

1143+
#[test]
1144+
fn openapi_spec_has_security_schemes() {
1145+
let spec = spec();
1146+
assert_json_snapshot!(spec["components"]["securitySchemes"], @r###"
1147+
{
1148+
"bearerAuth": {
1149+
"type": "http",
1150+
"scheme": "bearer",
1151+
"bearerFormat": "JWT",
1152+
"description": "JWT token obtained from the authentication service. Pass as `Authorization: Bearer <token>`."
1153+
}
1154+
}
1155+
"###);
1156+
}
1157+
1158+
#[test]
1159+
fn openapi_spec_has_global_security() {
1160+
let spec = spec();
1161+
assert_json_snapshot!(spec["security"], @r###"
1162+
[
1163+
{
1164+
"bearerAuth": []
1165+
}
1166+
]
1167+
"###);
1168+
}
1169+
1170+
#[test]
1171+
fn openapi_spec_anonymous_endpoint_overrides_security() {
1172+
let spec = spec();
1173+
let status_get = &spec["paths"]["/api/status"]["get"];
1174+
assert_json_snapshot!(status_get["security"], @r###"
1175+
[
1176+
{}
1177+
]
1178+
"###);
1179+
}
1180+
1181+
#[test]
1182+
fn openapi_spec_optional_auth_endpoint_has_both_options() {
1183+
let spec = spec();
1184+
let template_get = &spec["paths"]["/api/certificates/templates/{template_id}"]["get"];
1185+
assert_json_snapshot!(template_get["security"], @r###"
1186+
[
1187+
{},
1188+
{
1189+
"bearerAuth": []
1190+
}
1191+
]
1192+
"###);
1193+
}
1194+
10961195
#[test]
10971196
fn openapi_spec_has_external_docs() {
10981197
let spec = spec();

src/server/handlers/api_trackers.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ pub struct TrackerIdPath {
2020
#[utoipa::path(
2121
tags = ["web_scraping"],
2222
responses(
23-
(status = 200, description = "List of API trackers.", body = [ApiTracker])
23+
(status = 200, description = "List of API trackers.", body = [ApiTracker]),
24+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
2425
)
2526
)]
2627
#[get("/api/web_scraping/api_trackers")]
@@ -38,7 +39,8 @@ pub async fn api_trackers_list(
3839
request_body = ApiTrackerCreateParams,
3940
responses(
4041
(status = 201, description = "API tracker was successfully created.", body = ApiTracker),
41-
(status = BAD_REQUEST, description = "Invalid API tracker parameters.")
42+
(status = BAD_REQUEST, description = "Invalid API tracker parameters."),
43+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
4244
)
4345
)]
4446
#[post("/api/web_scraping/api_trackers")]
@@ -62,7 +64,8 @@ pub async fn api_trackers_create(
6264
request_body = ApiTrackerUpdateParams,
6365
responses(
6466
(status = 204, description = "API tracker was successfully updated."),
65-
(status = NOT_FOUND, description = "API tracker not found.")
67+
(status = NOT_FOUND, description = "API tracker not found."),
68+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
6669
)
6770
)]
6871
#[put("/api/web_scraping/api_trackers/{tracker_id}")]
@@ -86,7 +89,8 @@ pub async fn api_trackers_update(
8689
params(TrackerIdPath),
8790
responses(
8891
(status = 204, description = "API tracker was successfully deleted."),
89-
(status = NOT_FOUND, description = "API tracker not found.")
92+
(status = NOT_FOUND, description = "API tracker not found."),
93+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
9094
)
9195
)]
9296
#[delete("/api/web_scraping/api_trackers/{tracker_id}")]
@@ -110,7 +114,8 @@ pub async fn api_trackers_delete(
110114
request_body = ApiTrackerGetHistoryParams,
111115
responses(
112116
(status = 200, description = "List of API tracker revisions."),
113-
(status = NOT_FOUND, description = "API tracker not found.")
117+
(status = NOT_FOUND, description = "API tracker not found."),
118+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
114119
)
115120
)]
116121
#[post("/api/web_scraping/api_trackers/{tracker_id}/_history")]
@@ -134,7 +139,8 @@ pub async fn api_trackers_get_history(
134139
params(TrackerIdPath),
135140
responses(
136141
(status = 204, description = "History was successfully cleared."),
137-
(status = NOT_FOUND, description = "API tracker not found.")
142+
(status = NOT_FOUND, description = "API tracker not found."),
143+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
138144
)
139145
)]
140146
#[post("/api/web_scraping/api_trackers/{tracker_id}/_clear")]
@@ -157,7 +163,8 @@ pub async fn api_trackers_clear_history(
157163
params(TrackerIdPath),
158164
responses(
159165
(status = 200, description = "List of tracker execution logs."),
160-
(status = NOT_FOUND, description = "API tracker not found.")
166+
(status = NOT_FOUND, description = "API tracker not found."),
167+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
161168
)
162169
)]
163170
#[get("/api/web_scraping/api_trackers/{tracker_id}/_logs")]
@@ -180,7 +187,8 @@ pub async fn api_trackers_get_logs(
180187
params(TrackerIdPath),
181188
responses(
182189
(status = 204, description = "Logs were successfully cleared."),
183-
(status = NOT_FOUND, description = "API tracker not found.")
190+
(status = NOT_FOUND, description = "API tracker not found."),
191+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
184192
)
185193
)]
186194
#[post("/api/web_scraping/api_trackers/{tracker_id}/_clear_logs")]
@@ -201,7 +209,8 @@ pub async fn api_trackers_clear_logs(
201209
#[utoipa::path(
202210
tags = ["web_scraping"],
203211
responses(
204-
(status = 200, description = "Logs summary keyed by tracker ID.")
212+
(status = 200, description = "Logs summary keyed by tracker ID."),
213+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
205214
)
206215
)]
207216
#[get("/api/web_scraping/api_trackers/_logs_summary")]
@@ -223,7 +232,8 @@ pub async fn api_trackers_get_logs_summary(
223232
request_body = ApiTrackerTestParams,
224233
responses(
225234
(status = 200, description = "Test request result.", body = ApiTrackerTestResult),
226-
(status = BAD_REQUEST, description = "Invalid test parameters.")
235+
(status = BAD_REQUEST, description = "Invalid test parameters."),
236+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
227237
)
228238
)]
229239
#[post("/api/web_scraping/api_trackers/_test")]
@@ -246,7 +256,8 @@ pub async fn api_trackers_test(
246256
request_body = ApiTrackerDebugParams,
247257
responses(
248258
(status = 200, description = "Debug result."),
249-
(status = BAD_REQUEST, description = "Invalid debug parameters.")
259+
(status = BAD_REQUEST, description = "Invalid debug parameters."),
260+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
250261
)
251262
)]
252263
#[post("/api/web_scraping/api_trackers/_debug")]

src/server/handlers/certificate_templates.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ pub struct CertificateTemplateGetResponse {
3131
#[utoipa::path(
3232
tags = ["certificates"],
3333
responses(
34-
(status = 200, description = "List of certificate templates.", body = [CertificateTemplate])
34+
(status = 200, description = "List of certificate templates.", body = [CertificateTemplate]),
35+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
3536
)
3637
)]
3738
#[get("/api/certificates/templates")]
@@ -55,6 +56,7 @@ pub async fn certificate_templates_list(
5556
#[utoipa::path(
5657
tags = ["certificates"],
5758
params(TemplateIdPath),
59+
security((), ("bearerAuth" = [])),
5860
responses(
5961
(status = 200, description = "Certificate template with share info.", body = CertificateTemplateGetResponse),
6062
(status = 404, description = "Template not found.")
@@ -105,7 +107,8 @@ pub async fn certificate_templates_get(
105107
request_body = TemplatesCreateParams,
106108
responses(
107109
(status = 201, description = "Template was successfully created.", body = CertificateTemplate),
108-
(status = BAD_REQUEST, description = "Invalid template parameters.")
110+
(status = BAD_REQUEST, description = "Invalid template parameters."),
111+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
109112
)
110113
)]
111114
#[post("/api/certificates/templates")]
@@ -129,7 +132,8 @@ pub async fn certificate_templates_create(
129132
request_body = TemplatesUpdateParams,
130133
responses(
131134
(status = 204, description = "Template was successfully updated."),
132-
(status = NOT_FOUND, description = "Template not found.")
135+
(status = NOT_FOUND, description = "Template not found."),
136+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
133137
)
134138
)]
135139
#[put("/api/certificates/templates/{template_id}")]
@@ -153,7 +157,8 @@ pub async fn certificate_templates_update(
153157
params(TemplateIdPath),
154158
responses(
155159
(status = 204, description = "Template was successfully deleted."),
156-
(status = NOT_FOUND, description = "Template not found.")
160+
(status = NOT_FOUND, description = "Template not found."),
161+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
157162
)
158163
)]
159164
#[delete("/api/certificates/templates/{template_id}")]
@@ -177,6 +182,7 @@ pub async fn certificate_templates_delete(
177182
tags = ["certificates"],
178183
params(TemplateIdPath),
179184
request_body = TemplatesGenerateParams,
185+
security((), ("bearerAuth" = [])),
180186
responses(
181187
(status = 200, description = "Generated certificate data (binary, base64-encoded in JSON)."),
182188
(status = NOT_FOUND, description = "Template not found.")
@@ -212,7 +218,8 @@ pub async fn certificate_templates_generate(
212218
params(TemplateIdPath),
213219
responses(
214220
(status = 200, description = "Share info for the template."),
215-
(status = NOT_FOUND, description = "Template not found.")
221+
(status = NOT_FOUND, description = "Template not found."),
222+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
216223
)
217224
)]
218225
#[post("/api/certificates/templates/{template_id}/_share")]
@@ -235,7 +242,8 @@ pub async fn certificate_templates_share(
235242
tags = ["certificates"],
236243
params(TemplateIdPath),
237244
responses(
238-
(status = 204, description = "Template was successfully unshared.")
245+
(status = 204, description = "Template was successfully unshared."),
246+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
239247
)
240248
)]
241249
#[post("/api/certificates/templates/{template_id}/_unshare")]
@@ -258,7 +266,8 @@ pub async fn certificate_templates_unshare(
258266
request_body = TemplatesFetchCertificatesParams,
259267
responses(
260268
(status = 200, description = "PEM-encoded certificate chain.", body = [String]),
261-
(status = BAD_REQUEST, description = "Invalid or unreachable URL.")
269+
(status = BAD_REQUEST, description = "Invalid or unreachable URL."),
270+
(status = UNAUTHORIZED, description = "Missing or invalid authentication credentials.")
262271
)
263272
)]
264273
#[post("/api/certificates/_fetch")]

0 commit comments

Comments
 (0)