ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
1 changed files with 145336 additions and 0 deletions
188
services/mirrored-image/k8s/bootstrap/quay-tests.fsx
Normal file
188
services/mirrored-image/k8s/bootstrap/quay-tests.fsx
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#load "gateway.fsx"
|
||||
|
||||
open System
|
||||
open System.Net
|
||||
open System.Net.Http
|
||||
open System.Net.Http.Headers
|
||||
open System.Security.Cryptography
|
||||
open System.Text
|
||||
open System.Text.Json
|
||||
open System.Threading
|
||||
open System.Threading.Tasks
|
||||
open Bootstrap
|
||||
open Gateway
|
||||
|
||||
let require condition message =
|
||||
if not condition then
|
||||
invalidOp message
|
||||
|
||||
let base64Bytes (value: string) =
|
||||
let value = value.Replace('-', '+').Replace('_', '/')
|
||||
let padded = value.PadRight(value.Length + ((4 - value.Length % 4) % 4), '=')
|
||||
Convert.FromBase64String padded
|
||||
|
||||
type QuayHandler(rsa: RSA, wrongScopes: bool) =
|
||||
inherit HttpMessageHandler()
|
||||
|
||||
let mutable assigned = false
|
||||
let seen = ResizeArray<string>()
|
||||
member _.Seen = seen
|
||||
|
||||
member private _.Respond(request: HttpRequestMessage) =
|
||||
let path = request.RequestUri.AbsolutePath
|
||||
seen.Add($"{request.Method} {path}")
|
||||
|
||||
if request.RequestUri.Host = "zitadel-api" then
|
||||
require (request.Headers.Host = "zitadel") "canonical ZITADEL host changed"
|
||||
|
||||
require
|
||||
(request.Headers.GetValues("X-Zitadel-Public-Host") |> Seq.exactlyOne = "zitadel")
|
||||
"public ZITADEL host changed"
|
||||
|
||||
if path.StartsWith "/v2/" then
|
||||
require (request.Headers.Authorization.Scheme = "Bearer") "ZITADEL system JWT was omitted"
|
||||
let segments = request.Headers.Authorization.Parameter.Split('.')
|
||||
require (segments.Length = 3) "JWT segment count changed"
|
||||
use header = JsonDocument.Parse(base64Bytes segments[0])
|
||||
use claims = JsonDocument.Parse(base64Bytes segments[1])
|
||||
require (header.RootElement.GetProperty("alg").GetString() = "RS256") "JWT algorithm changed"
|
||||
require (claims.RootElement.GetProperty("iss").GetString() = "login-client") "JWT issuer changed"
|
||||
require (claims.RootElement.GetProperty("sub").GetString() = "login-client") "JWT subject changed"
|
||||
require (claims.RootElement.GetProperty("aud").GetString() = "http://zitadel") "JWT audience changed"
|
||||
let issued = claims.RootElement.GetProperty("iat").GetInt64()
|
||||
require (claims.RootElement.GetProperty("exp").GetInt64() = issued + 300L) "JWT lifetime changed"
|
||||
|
||||
require
|
||||
(rsa.VerifyData(
|
||||
Encoding.UTF8.GetBytes($"{segments[0]}.{segments[1]}"),
|
||||
base64Bytes segments[2],
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1
|
||||
))
|
||||
"JWT signature is invalid"
|
||||
|
||||
let status, body, location =
|
||||
match request.Method.Method, path with
|
||||
| "GET", "/csrf_token" -> HttpStatusCode.OK, """{"csrf_token":"csrf"}""", None
|
||||
| "POST", "/api/v1/externallogin/zitadel" ->
|
||||
require (request.Headers.GetValues("X-CSRF-Token") |> Seq.exactlyOne = "csrf") "Quay CSRF token changed"
|
||||
require (isNull request.Content.Headers.ContentType.CharSet) "Quay JSON media type included a charset"
|
||||
require (request.Content.Headers.ContentLength.HasValue) "Quay JSON request omitted its content length"
|
||||
HttpStatusCode.OK, """{"auth_url":"http://zitadel/oidc/start"}""", None
|
||||
| "GET", "/oidc/start" -> HttpStatusCode.Found, "", Some "/ui/v2/login/login?authRequest=request-id"
|
||||
| "POST", "/v2/sessions" -> HttpStatusCode.Created, """{"sessionId":"session-id"}""", None
|
||||
| "PATCH", "/v2/sessions/session-id" -> HttpStatusCode.OK, """{"sessionToken":"session-token"}""", None
|
||||
| "POST", "/v2/oidc/auth_requests/request-id" ->
|
||||
HttpStatusCode.OK, """{"callbackUrl":"http://quay/callback"}""", None
|
||||
| "GET", "/callback" -> HttpStatusCode.Found, "", Some "http://quay/"
|
||||
| "PUT", "/api/v1/organization/candidates/team/mirrored-image" -> HttpStatusCode.OK, "", None
|
||||
| "PUT", "/api/v1/organization/candidates/team/mirrored-image/members/candidates-bot" ->
|
||||
HttpStatusCode.OK, "", None
|
||||
| "GET", "/api/v1/organization/candidates/prototypes" -> HttpStatusCode.OK, """{"prototypes":[]}""", None
|
||||
| "POST", "/api/v1/organization/candidates/prototypes" -> HttpStatusCode.OK, "", None
|
||||
| "GET", "/api/v1/organization/candidates/applications" ->
|
||||
HttpStatusCode.OK, """{"applications":[]}""", None
|
||||
| "POST", "/api/v1/organization/candidates/applications" ->
|
||||
HttpStatusCode.OK, """{"client_id":"client-id"}""", None
|
||||
| "GET", "/api/v1/user/assignedauthorization" when not assigned ->
|
||||
HttpStatusCode.OK, """{"authorizations":[]}""", None
|
||||
| "POST", "/oauth/authorize/assignuser" ->
|
||||
require (request.Headers.Authorization.Scheme = "Basic") "admin token did not use Quay Basic auth"
|
||||
let query = Uri.UnescapeDataString request.RequestUri.Query
|
||||
require (query.Contains "scope=repo:create+repo:write") "writer scopes changed"
|
||||
require (not (query.Contains "repo:read")) "writer token redundantly requested read"
|
||||
assigned <- true
|
||||
HttpStatusCode.OK, "", None
|
||||
| "GET", "/api/v1/user/assignedauthorization" ->
|
||||
let scopes =
|
||||
if wrongScopes then
|
||||
"""[{"scope":"repo:read"}]"""
|
||||
else
|
||||
"""[{"scope":"repo:create"},{"scope":"repo:write"}]"""
|
||||
|
||||
HttpStatusCode.OK,
|
||||
sprintf
|
||||
"""{"authorizations":[{"application":{"clientId":"client-id"},"uuid":"assignment-id","redirectUri":"http://quay/oauth/localapp","responseType":"token","scopes":%s}]}"""
|
||||
scopes,
|
||||
None
|
||||
| "GET", "/oauth/authorize" -> HttpStatusCode.OK, """{"csrf_token_val":"approval-csrf"}""", None
|
||||
| "POST", "/oauth/authorizeapp" ->
|
||||
require
|
||||
(request.Headers.GetValues("X-CSRF-Token") |> Seq.exactlyOne = "approval-csrf")
|
||||
"approval CSRF token changed"
|
||||
|
||||
HttpStatusCode.Found, "", Some "http://quay/oauth/localapp#access_token=issued-token"
|
||||
| _ -> invalidOp $"unexpected request {request.Method} {request.RequestUri}"
|
||||
|
||||
let response = new HttpResponseMessage(status)
|
||||
response.Content <- new StringContent(body, Encoding.UTF8, "application/json")
|
||||
|
||||
location
|
||||
|> Option.iter (fun value -> response.Headers.Location <- Uri(value, UriKind.RelativeOrAbsolute))
|
||||
|
||||
response
|
||||
|
||||
override this.Send(request: HttpRequestMessage, _: CancellationToken) = this.Respond request
|
||||
|
||||
override this.SendAsync(request: HttpRequestMessage, _: CancellationToken) = Task.FromResult(this.Respond request)
|
||||
|
||||
let rsa = RSA.Create(2048)
|
||||
let handler = new QuayHandler(rsa, false)
|
||||
let adminClient = new HttpClient(handler, false)
|
||||
|
||||
let sessionClient () = new HttpClient(handler, false)
|
||||
|
||||
let quay =
|
||||
makeQuayWith
|
||||
adminClient
|
||||
sessionClient
|
||||
{ URL = "http://quay"
|
||||
ZitadelAPI = "http://zitadel-api"
|
||||
ZitadelIssuer = "http://zitadel"
|
||||
CaPath = ""
|
||||
LoginKey = rsa.ExportRSAPrivateKeyPem() }
|
||||
|
||||
let session = quay.Login "candidates-bot" "password" (quay.LoginAuth())
|
||||
let definition = apps |> List.find (fun app -> app.Name = "candidates-writer")
|
||||
quay.Access "admin-token" definition
|
||||
let clientID = quay.Application "admin-token" definition
|
||||
let app = { definition with Client = clientID }
|
||||
quay.Assign "admin-token" session app
|
||||
let token = quay.Issue session app
|
||||
require (token = "issued-token") "issued Quay token changed"
|
||||
require (handler.Seen.Count = 19) "bootstrap request sequence changed"
|
||||
adminClient.Dispose()
|
||||
|
||||
let wrongHandler = new QuayHandler(rsa, true)
|
||||
let wrongAdmin = new HttpClient(wrongHandler, false)
|
||||
|
||||
let wrongQuay =
|
||||
makeQuayWith
|
||||
wrongAdmin
|
||||
(fun () -> new HttpClient(wrongHandler, false))
|
||||
{ URL = "http://quay"
|
||||
ZitadelAPI = "http://zitadel-api"
|
||||
ZitadelIssuer = "http://zitadel"
|
||||
CaPath = ""
|
||||
LoginKey = rsa.ExportRSAPrivateKeyPem() }
|
||||
|
||||
let wrongSession =
|
||||
wrongQuay.Login "candidates-bot" "password" (wrongQuay.LoginAuth())
|
||||
|
||||
wrongQuay.Access "admin-token" definition
|
||||
|
||||
let wrongApp =
|
||||
{ definition with
|
||||
Client = wrongQuay.Application "admin-token" definition }
|
||||
|
||||
let scopeRejected =
|
||||
try
|
||||
wrongQuay.Assign "admin-token" wrongSession wrongApp
|
||||
false
|
||||
with :? InvalidOperationException as error ->
|
||||
error.Message = "assigned authorization has different scopes"
|
||||
|
||||
require scopeRejected "wrong assigned scopes were accepted"
|
||||
wrongAdmin.Dispose()
|
||||
rsa.Dispose()
|
||||
printfn "Quay bootstrap tests passed"
|
||||
Loading…
Reference in a new issue