From 2cfb91aaebc4e0d4eab9f6f418a9566fa23cf0bc Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Fri, 17 Mar 2017 23:34:33 -0500 Subject: [PATCH] WIP Adding session auth and routes. --- README.md | 5 +- api.rst | 18 +++---- src/main/nim/strawboss/configuration.nim | 39 ++++++++++++--- src/main/nim/strawboss/server.nim | 63 +++++++++++++++++++++++- strawboss.config.json | 2 +- strawboss.nimble | 1 + 6 files changed, 107 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 3773f37..2e8179f 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,11 @@ are: * `artifactsRepo`: A string denoting the path to the artifacts repository directory. +* `authSecret`: Secret key used to sign JWT session tokens. + * `users`: the array of user definition objects. Each user object is required to have `username` and `hashedPwd` keys, both string. -* `tokens`: an array of string, each representing a valid auth token that has - been issued to a client. - * `projects`: an array of project definitions (detailed below). All are required. diff --git a/api.rst b/api.rst index 06a1488..0e4bd0e 100644 --- a/api.rst +++ b/api.rst @@ -1,9 +1,9 @@ -GET /api/ping -POST /api/auth-token -GET /api/projects -- return project summaries -POST /api/projects -- create a new project -GET /api/project/ -- return detailed project record (include steps) -GET /api/project// -- return detailed step information (include runs) -POST /api/project///run/ -- kick off a run -GET /api/project///run/ -- return detailed run information - +✓ GET /api/ping +- POST /api/auth-token +✓ GET /api/projects -- return project summaries +- POST /api/projects -- create a new project +- GET /api/project/ -- return detailed project record (include steps) +- GET /api/project//active -- return detailed information about all currently running runs +- GET /api/project// -- return detailed step information (include runs) +- POST /api/project///run/ -- kick off a run +- GET /api/project///run/ -- return detailed run information diff --git a/src/main/nim/strawboss/configuration.nim b/src/main/nim/strawboss/configuration.nim index 0935497..d548200 100644 --- a/src/main/nim/strawboss/configuration.nim +++ b/src/main/nim/strawboss/configuration.nim @@ -1,4 +1,4 @@ -import logging, json, os, nre, sequtils, strtabs, tables +import logging, json, os, nre, sequtils, strtabs, tables, times import private/util # Types @@ -21,14 +21,23 @@ type cfgFilePath*, defaultBranch*, name*, repo*: string envVars*: StringTableRef - StrawBossConfig* = object - artifactsRepo*: string - projects*: seq[ProjectDef] - RunRequest* = object projectName*, stepName*, buildRef*, workspaceDir*: string forceRebuild*: bool + User* = object + name*: string + hashedPwd*: string + + UserRef* = ref User + + StrawBossConfig* = object + artifactsRepo*: string + authSecret*: string + projects*: seq[ProjectDef] + users*: seq[UserRef] + + # internal utils let nullNode = newJNull() @@ -62,9 +71,18 @@ proc loadStrawBossConfig*(cfgFile: string): StrawBossConfig = envVars: envVars, repo: pJson.getOrFail("repo", "project definition").getStr)) + var users: seq[UserRef] = @[] + + for uJson in jsonCfg.getIfExists("users").getElems: + users.add(UserRef( + name: uJson.getOrFail("name", "user record").getStr, + hashedPwd: uJson.getOrFail("hashedPwd", "user record").getStr)) + result = StrawBossConfig( artifactsRepo: jsonCfg.getIfExists("artifactsRepo").getStr("artifacts"), - projects: projectDefs) + authSecret: jsonCfg.getOrFail("authSecret", "strawboss config").getStr, + projects: projectDefs, + users: users) proc loadProjectConfig*(cfgFile: string): ProjectCfg = if not existsFile(cfgFile): @@ -120,6 +138,15 @@ proc `%`*(s: BuildStatus): JsonNode = "details": s.details } +proc `%`*(p: ProjectDef): JsonNode = + result = %* { + "name": p.name, + "cfgFilePath": p.cfgFilePath, + "defaultBranch": p.defaultBranch, + "repo": p.repo } + + # TODO: envVars? + proc `%`*(req: RunRequest): JsonNode = result = %* { "projectName": req.projectName, diff --git a/src/main/nim/strawboss/server.nim b/src/main/nim/strawboss/server.nim index 85023ad..100a299 100644 --- a/src/main/nim/strawboss/server.nim +++ b/src/main/nim/strawboss/server.nim @@ -1,4 +1,4 @@ -import asyncdispatch, jester, json, osproc, tempfile +import asyncdispatch, jester, json, jwt, osproc, sequtils, tempfile, times import ./configuration, ./core, private/util @@ -9,6 +9,60 @@ type Worker = object process*: Process workingDir*: string +type + Session = object + user*: UserRef + issuedAt*, expires*: TimeInfo + +const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" + +proc makeJsonResp(status: HttpCode): string = + result = $(%* { + "statusCode": status.int, + "status": $status + }) + +proc newSession(user: UserRef): Session = + result = Session( + user: user, + issuedAt: getGMTime(getTime()), + expires: daysForward(7)) + +proc toJWT(session: Session): JWT = + result = JWT( + header: JOSEHeader(alg: HS256, typ: "jwt"), + claims: toClaims(%*{ + "sub": session.user.name, + "iss": session.issuedAt.format(ISO_TIME_FORMAT), + "exp": session.expires.format(ISO_TIME_FORMAT) })) + +proc extractSession(cfg: StrawBossConfig, request: Request): Session = + + # Find the auth header + if not request.headers.hasKey("Authentication"): + raiseEx "No auth token." + + # Read and verify the JWT token + let jwt = toJWT(request.headers["Authentication"]) + 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: parse(jwt.claims["iat"].node.str, ISO_TIME_FORMAT), + expires: parse(jwt.claims["exp"].node.str, ISO_TIME_FORMAT)) + +template requireAuth() = + var session {.inject.}: Session + try: session = extractSession(givenCfg, request) + except: resp Http401, makeJsonResp(Http401), "application/json" + proc spawnWorker(req: RunRequest): Worker = let dir = mkdtemp() var args = @["run", req.projectName, req.stepName, "-r", req.buildRef, "-w", dir] @@ -25,8 +79,13 @@ proc start*(givenCfg: StrawBossConfig): void = get "/api/ping": resp $(%*"pong"), "application/json" + get "/api/auth-token": + resp Http501, makeJsonResp(Http501), "application/json" + get "/api/projects": - resp $(%*[]), "application/json" + requireAuth() + #let projectDefs: seq[ProjectDef] = givenCfg.projects.mapIt(it) + resp $(%(givenCfg.projects)), "application/json" post "/api/project/@projectName/@stepName/run/@buildRef?": workers.add(spawnWorker(RunRequest( diff --git a/strawboss.config.json b/strawboss.config.json index 0811599..83745e5 100644 --- a/strawboss.config.json +++ b/strawboss.config.json @@ -1,7 +1,7 @@ { "artifactsRepo": "artifacts", "users": [], - "tokens": [], + "authSecret": "change me", "projects": [ { "name": "new-life-intro-band", "repo": "/home/jdb/projects/new-life-introductory-band" }, diff --git a/strawboss.nimble b/strawboss.nimble index be3cccc..2538f9d 100644 --- a/strawboss.nimble +++ b/strawboss.nimble @@ -10,4 +10,5 @@ srcDir = "src/main/nim" # Dependencies requires @["nim >= 0.16.1", "docopt >= 0.1.0", "tempfile", "jester"] +requires "https://github.com/yglukhov/nim-jwt"