224 lines
6.9 KiB
Nim

import algorithm, asyncdispatch, bcrypt, jester, json, jwt, os, osproc,
sequtils, strutils, tempfile, times, unittest
import logging
import ./configuration, ./core, private/util
type Worker = object
process*: Process
workingDir*: string
type
Session = object
user*: UserRef
issuedAt*, expires*: Time
#const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
const JSON = "application/json"
proc makeJsonResp(status: HttpCode, details: string = ""): string =
result = $(%* {
"statusCode": status.int,
"status": $status,
"details": details
})
proc newSession*(user: UserRef): Session =
result = Session(
user: user,
issuedAt: getTime(),
expires: daysForward(7).toTime())
proc toJWT*(cfg: StrawBossConfig, session: Session): string =
# result = toJWT(%* {
# "header": {
# "alg": "HS256",
# "typ": "JWT" },
# "claims": {
# "sub": session.user.name,
# "iat": session.issuedAt.toSeconds().int,
# "exp": session.expires.toSeconds().int } })
var jwt = JWT(
header: JOSEHeader(alg: HS256, typ: "jwt"),
claims: toClaims(%*{
"sub": session.user.name,
"iat": session.issuedAt.toSeconds().int,
"exp": session.expires.toSeconds().int }))
jwt.sign(cfg.authSecret)
result = $jwt
proc fromJWT*(cfg: StrawBossConfig, strTok: string): Session =
let jwt = toJWT(strTok)
var secret = cfg.authSecret
if not jwt.verify(secret): raiseEx "Unable to verify auth token."
jwt.verifyTimeClaims()
# Find the user record (if authenticated)
let username = jwt.claims["sub"].node.str
let users = cfg.users.filterIt(it.name == username)
if users.len != 1: raiseEx "Could not find session user."
result = Session(
user: users[0],
issuedAt: fromSeconds(jwt.claims["iat"].node.num),
expires: fromSeconds(jwt.claims["exp"].node.num))
proc extractSession(cfg: StrawBossConfig, request: Request): Session =
# Find the auth header
if not request.headers.hasKey("Authorization"):
raiseEx "No auth token."
# Read and verify the JWT token
let headerVal = request.headers["Authorization"]
if not headerVal.startsWith("Bearer "):
raiseEx "Invalid Authentication type (only 'Bearer' is supported)."
result = fromJWT(cfg, headerVal[7..^1])
proc spawnWorker(req: RunRequest): Worker =
let dir = mkdtemp()
var args = @["run", req.projectName, req.stepName, "-r", req.buildRef, "-w", dir]
if req.forceRebuild: args.add("-f")
result = Worker(
process: startProcess("strawboss", ".", args, loadEnv(), {poUsePath}),
workingDir: dir)
proc hashPwd*(pwd: string, cost: int8): string =
let salt = genSalt(cost)
result = hash(pwd, salt)
proc validatePwd*(u: UserRef, givenPwd: string): bool =
let salt = u.hashedPwd[0..28] # TODO: magic numbers
result = compare(u.hashedPwd, hash(givenPwd, salt))
proc makeAuthToken*(cfg: StrawBossConfig, uname, pwd: string): string =
if uname == nil or pwd == nil:
raiseEx "fields 'username' and 'password' required"
# find the user record
let users = cfg.users.filterIt(it.name == uname)
if users.len != 1: raiseEx "invalid username or password"
let user = users[0]
if not validatePwd(user, pwd): raiseEx "invalid username or password"
result = toJWT(cfg, newSession(user))
template withSession(body: untyped): untyped =
var session {.inject.}: Session
var authed = false
try:
session = extractSession(cfg, request)
authed = true
except:
debug "Auth failed: " & getCurrentExceptionMsg()
resp(Http401, makeJsonResp(Http401), JSON)
if authed: body
proc start*(cfg: StrawBossConfig): void =
let stopFuture = newFuture[void]()
var workers: seq[Worker] = @[]
settings:
port = Port(8180)
appName = "/api"
routes:
get "/ping": resp($(%*"pong"), JSON)
get "/auth-token":
try:
let authToken = makeAuthToken(cfg, @"username", @"password")
resp("\"" & $authToken & "\"", JSON)
except: resp(Http401, makeJsonResp(Http401, getCurrentExceptionMsg()), JSON)
get "/verify-auth": withSession:
resp(Http200, $(%*{ "username": session.user.name }), JSON)
get "/projects": withSession:
# List project summaries (ProjectDefs only)
resp($(%(cfg.projects)), JSON)
post "/projects": withSession:
# Create a new project definition
resp(Http501, makeJsonResp(Http501), JSON)
get "/project/@projectName/@version?": withSession:
## Get a detailed project record including step definitions (ProjectConfig).
# Make sure we know about that project
var project: ProjectDef
try: project = cfg.findProject(@"projectName")
except: resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
# Given version
var cachedFilePath: string
if @"version" != "":
cachedFilePath = cfg.artifactsRepo & "/" & project.name &
"/configuration." & @"version" & ".json"
if not existsFile(cachedFilePath):
resp(Http404,
makeJsonResp(Http404, "I have never built version " & @"version"),
JSON)
# No version requested, use "latest"
else:
let confFilePaths = toSeq(walkFiles("configuration.*.json"))
if confFilePaths.len == 0:
resp(Http404, makeJsonResp(Http404, "I have not built any versions of " & project.name), JSON)
let modTimes = confFilePaths.mapIt(it.getLastModificationTime)
cachedFilePath = sorted(zip(confFilePaths, modTimes),
proc (a, b: tuple): int = cmp(a.b, b.b))[0].a
try: resp(readFile(cachedFilePath), JSON)
except:
debug "Could not serve cached project configuration at: " &
cachedFilePath & "\n\t Reason: " & getCurrentExceptionMsg()
resp(Http500, makeJsonResp(Http500, "could not read cached project configuration"), JSON)
get "/api/project/@projectName/active": withSession:
# List all currently active runs
resp(Http501, makeJsonResp(Http501), JSON)
get "/api/project/@projectName/@stepName": withSession:
# Get step details including runs.
resp(Http501, makeJsonResp(Http501), JSON)
get "/api/project/@projectName/@stepName/run/@buildRef": withSession:
# Get detailed information about a run
resp(Http501, makeJsonResp(Http501), JSON)
post "/project/@projectName/@stepName/run/@buildRef?":
# Kick off a run
workers.add(spawnWorker(RunRequest(
projectName: @"projectName",
stepName: @"stepName",
buildRef: if @"buildRef" != "": @"buildRef" else: nil,
forceRebuild: false))) # TODO support this with optional query params
post "/service/debug/stop":
if not cfg.debug: resp(Http404, makeJsonResp(Http404), JSON)
else:
callSoon(proc(): void = complete(stopFuture))
resp($(%*"shutting down"), JSON)
#[
get re".*":
resp(Http404, makeJsonResp(Http404), JSON)
post re".*":
resp(Http404, makeJsonResp(Http404), JSON)
]#
waitFor(stopFuture)