Added ProjectDef parsing code. Unit test for , authentication logic.

This commit is contained in:
Jonathan Bernard 2017-04-24 16:31:58 -05:00
parent 053ac8dc14
commit ec967ec2bf
4 changed files with 162 additions and 43 deletions

View File

@ -1,6 +1,8 @@
import logging, json, os, nre, sequtils, strtabs, tables, times import logging, json, os, nre, sequtils, strtabs, tables, times
import private/util import private/util
from typeinfo import toAny
# Types # Types
# #
type type
@ -68,6 +70,17 @@ proc getOrFail(n: JsonNode, key: string, objName: string = ""): JsonNode =
return n[key] return n[key]
# Configuration parsing code # Configuration parsing code
proc parseProjectDef*(pJson: JsonNode): ProjectDef =
var envVars = newStringTable(modeCaseSensitive)
for k, v in pJson.getIfExists("envVars").getFields: envVars[k] = v.getStr("")
result = ProjectDef(
cfgFilePath: pJson.getIfExists("cfgFilePath").getStr("strawboss.json"),
defaultBranch: pJson.getIfExists("defaultBranch").getStr("master"),
name: pJson.getOrFail("name", "project definition").getStr,
envVars: envVars,
repo: pJson.getOrFail("repo", "project definition").getStr)
proc loadStrawBossConfig*(cfgFile: string): StrawBossConfig = proc loadStrawBossConfig*(cfgFile: string): StrawBossConfig =
if not existsFile(cfgFile): if not existsFile(cfgFile):
@ -75,20 +88,6 @@ proc loadStrawBossConfig*(cfgFile: string): StrawBossConfig =
let jsonCfg = parseFile(cfgFile) let jsonCfg = parseFile(cfgFile)
var projectDefs: seq[ProjectDef] = @[]
for pJson in jsonCfg.getIfExists("projects").getElems:
var envVars = newStringTable(modeCaseSensitive)
for k, v in pJson.getIfExists("envVars").getFields: envVars[k] = v.getStr("")
projectDefs.add(
ProjectDef(
cfgFilePath: pJson.getIfExists("cfgFilePath").getStr("strawboss.json"),
defaultBranch: pJson.getIfExists("defaultBranch").getStr("master"),
name: pJson.getOrFail("name", "project definition").getStr,
envVars: envVars,
repo: pJson.getOrFail("repo", "project definition").getStr))
var users: seq[UserRef] = @[] var users: seq[UserRef] = @[]
for uJson in jsonCfg.getIfExists("users").getElems: for uJson in jsonCfg.getIfExists("users").getElems:
@ -101,7 +100,7 @@ proc loadStrawBossConfig*(cfgFile: string): StrawBossConfig =
authSecret: jsonCfg.getOrFail("authSecret", "strawboss config").getStr, authSecret: jsonCfg.getOrFail("authSecret", "strawboss config").getStr,
debug: jsonCfg.getIfExists("debug").getBVal(false), debug: jsonCfg.getIfExists("debug").getBVal(false),
pwdCost: int8(jsonCfg.getOrFail("pwdCost", "strawboss config").getNum), pwdCost: int8(jsonCfg.getOrFail("pwdCost", "strawboss config").getNum),
projects: projectDefs, projects: jsonCfg.getIfExists("projects").getElems.mapIt(parseProjectDef(it)),
users: users) users: users)
proc loadProjectConfig*(cfgFile: string): ProjectConfig = proc loadProjectConfig*(cfgFile: string): ProjectConfig =
@ -141,6 +140,7 @@ proc loadBuildStatus*(statusFile: string): BuildStatus =
state: jsonObj.getOrFail("state", "build status").getStr, state: jsonObj.getOrFail("state", "build status").getStr,
details: jsonObj.getIfExists("details").getStr("") ) details: jsonObj.getIfExists("details").getStr("") )
# TODO: unused and untested, add tests if we start using this # TODO: unused and untested, add tests if we start using this
proc parseRunRequest*(reqStr: string): RunRequest = proc parseRunRequest*(reqStr: string): RunRequest =
let reqJson = parseJson(reqStr) let reqJson = parseJson(reqStr)
@ -166,7 +166,8 @@ proc `%`*(p: ProjectDef): JsonNode =
"defaultBranch": p.defaultBranch, "defaultBranch": p.defaultBranch,
"repo": p.repo } "repo": p.repo }
# TODO: envVars? result["envVars"] = newJObject()
for k, v in p.envVars: result["envVars"][k] = %v
proc `%`*(req: RunRequest): JsonNode = proc `%`*(req: RunRequest): JsonNode =
result = %* { result = %* {
@ -178,3 +179,18 @@ proc `%`*(req: RunRequest): JsonNode =
proc `$`*(s: BuildStatus): string = result = pretty(%s) proc `$`*(s: BuildStatus): string = result = pretty(%s)
proc `$`*(req: RunRequest): string = result = pretty(%req) proc `$`*(req: RunRequest): string = result = pretty(%req)
proc `$`*(pd: ProjectDef): string = result = pretty(%pd)
# TODO: maybe a macro for more general-purpose, shallow object comparison?
#proc `==`*(a, b: ProjectDef): bool =
template shallowEquals(a, b: RootObj): bool =
if type(a) != type(b): return false
var anyB = toAny(b)
for name, value in a.fieldPairs:
if value != b[name]: return false
return true
#proc `==`*(a, b: ProjectDef): bool = result = shallowEquals(a, b)

View File

@ -1,6 +1,7 @@
import asyncdispatch, bcrypt, jester, json, jwt, os, osproc, sequtils, tempfile, import asyncdispatch, bcrypt, jester, json, jwt, os, osproc, sequtils,
times, unittest strutils, tempfile, times, unittest
import logging
import ./configuration, ./core, private/util import ./configuration, ./core, private/util
type Worker = object type Worker = object
@ -47,7 +48,7 @@ proc toJWT*(cfg: StrawBossConfig, session: Session): string =
jwt.sign(cfg.authSecret) jwt.sign(cfg.authSecret)
result = $jwt result = $jwt
proc fromJWT*(cfg: StrawBossConfig, strTok: string): Session = proc fromJWT*(cfg: StrawBossConfig, strTok: string): Session =
let jwt = toJWT(strTok) let jwt = toJWT(strTok)
var secret = cfg.authSecret var secret = cfg.authSecret
if not jwt.verify(secret): raiseEx "Unable to verify auth token." if not jwt.verify(secret): raiseEx "Unable to verify auth token."
@ -66,11 +67,15 @@ proc fromJWT*(cfg: StrawBossConfig, strTok: string): Session =
proc extractSession(cfg: StrawBossConfig, request: Request): Session = proc extractSession(cfg: StrawBossConfig, request: Request): Session =
# Find the auth header # Find the auth header
if not request.headers.hasKey("Authentication"): if not request.headers.hasKey("Authorization"):
raiseEx "No auth token." raiseEx "No auth token."
# Read and verify the JWT token # Read and verify the JWT token
result = fromJWT(cfg, request.headers["Authentication"]) 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 = proc spawnWorker(req: RunRequest): Worker =
let dir = mkdtemp() let dir = mkdtemp()
@ -101,10 +106,19 @@ proc makeAuthToken*(cfg: StrawBossConfig, uname, pwd: string): string =
if not validatePwd(user, pwd): raiseEx "invalid username or password" if not validatePwd(user, pwd): raiseEx "invalid username or password"
result = toJWT(cfg, newSession(user)) result = toJWT(cfg, newSession(user))
template requireAuth() =
template withSession(body: untyped): untyped =
var session {.inject.}: Session var session {.inject.}: Session
try: session = extractSession(givenCfg, request) var authed = false
except: resp(Http401, makeJsonResp(Http401), "application/json")
try:
session = extractSession(givenCfg, request)
authed = true
except:
debug "Auth failed: " & getCurrentExceptionMsg()
resp(Http401, makeJsonResp(Http401), "application/json")
if authed: body
proc start*(givenCfg: StrawBossConfig): void = proc start*(givenCfg: StrawBossConfig): void =
@ -116,18 +130,22 @@ proc start*(givenCfg: StrawBossConfig): void =
appName = "/api" appName = "/api"
routes: routes:
get "/ping": get "/ping":
resp($(%*"pong"), "application/json") resp($(%*"pong"), "application/json")
get "/auth-token": get "/auth-token":
echo $request.params
try: try:
let authToken = makeAuthToken(givenCfg, @"username", @"password") let authToken = makeAuthToken(givenCfg, @"username", @"password")
resp("\"" & $authToken & "\"", "application/json") resp("\"" & $authToken & "\"", "application/json")
except: resp(Http401, makeJsonResp(Http401, getCurrentExceptionMsg())) except: resp(Http401, makeJsonResp(Http401, getCurrentExceptionMsg()))
get "/projects": get "/verify-auth": withSession:
requireAuth() resp(Http200, $(%*{
"username": session.user.name
}), "application/json")
get "/projects": withSession:
resp($(%(givenCfg.projects)), "application/json") resp($(%(givenCfg.projects)), "application/json")
post "/project/@projectName/@stepName/run/@buildRef?": post "/project/@projectName/@stepName/run/@buildRef?":

View File

@ -1,9 +1,61 @@
import strtabs, tables, unittest import json, strtabs, tables, unittest
import ./testutil import ./testutil
import ../../main/nim/strawbosspkg/configuration import ../../main/nim/strawbosspkg/configuration
suite "load and save configuration objects": suite "load and save configuration objects":
# suite setup & common data
let testProjDefStr = """{ "name": "test-project-1", "repo":
"/non-existent/dir",
"cfgFilePath": "strawhat.json",
"defaultBranch": "deploy",
"envVars": { "VAR1": "value" } }"""
let testProjDef = ProjectDef(
name: "test-project-1",
repo: "/non-existent/dir",
cfgFilePath: "strawhat.json",
defaultBranch: "deploy",
envVars: newStringTable("VAR1", "value", modeCaseInsensitive))
test "parseProjectDef":
let pd = parseProjectDef(parseJson(testProjDefStr))
check:
pd.name == "test-project-1"
pd.repo == "/non-existent/dir"
pd.cfgFilePath == "strawhat.json"
pd.defaultBranch == "deploy"
pd.envVars.len == 1
pd.envVars.hasKey("VAR1")
pd.envVars["VAR1"] == "value"
test "ProjectDef ==":
let pd1 = parseProjectDef(parseJson(testProjDefStr))
check pd1 == testProjDef
test "ProjectDef != (name)":
var pd1 = testProjDef
pd1.name = "different"
check pd1 != testProjDef
test "ProjectDef != (repo)":
var pd1 = testProjDef
pd1.repo = "different"
check pd1 != testProjDef
test "ProjectDef != (cfgFilePath)":
var pd1 = testProjDef
pd1.cfgFilePath = "different"
check pd1 != testProjDef
test "ProjectDef != (defaultBranch)":
var pd1 = testProjDef
pd1.defaultBranch = "different"
check pd1 != testProjDef
test "loadStrawBossConfig": test "loadStrawBossConfig":
let cfg = loadStrawBossConfig("src/test/json/strawboss.config.json") let cfg = loadStrawBossConfig("src/test/json/strawboss.config.json")
let expectedUsers = @[UserRef(name: "bob@builder.com", hashedPwd: "testvalue"), let expectedUsers = @[UserRef(name: "bob@builder.com", hashedPwd: "testvalue"),

View File

@ -1,10 +1,19 @@
import asyncdispatch, httpclient, os, osproc, strutils, times, unittest import asyncdispatch, httpclient, json, os, osproc, sequtils, strutils, times, unittest
import ./testutil import ./testutil
import ../../main/nim/strawbosspkg/configuration import ../../main/nim/strawbosspkg/configuration
import ../../main/nim/strawbosspkg/server import ../../main/nim/strawbosspkg/server
import ../../main/nim/strawbosspkg/private/util import ../../main/nim/strawbosspkg/private/util
suite "strawboss server": import strtabs
# test helpers
proc newAuthenticatedHttpClient(apiBase, uname, pwd: string): HttpClient =
result = newHttpClient()
let authResp = result.get(apiBase & "/auth-token?username=" & uname & "&password=" & pwd)
assert authResp.status.startsWith("200")
result.headers = newHttpHeaders({"Authorization": "Bearer " & parseJson(authResp.body).getStr})
suite "strawboss server can...":
# suite setup code # suite setup code
let cfgFilePath = "src/test/json/strawboss.config.json" let cfgFilePath = "src/test/json/strawboss.config.json"
@ -24,35 +33,59 @@ suite "strawboss server":
## UNIT TESTS ## UNIT TESTS
test "can validate hashed pwd": test "validate hashed pwd":
check validatePwd(testuser, "password") check validatePwd(testuser, "password")
test "can detect invalid pwds": test "detect invalid pwds":
check(not validatePwd(testuser, "Password")) check(not validatePwd(testuser, "Password"))
test "can make and extract a JWT token from a session": test "make and extract a JWT token from a session":
let session = newSession(testuser) let session = newSession(testuser)
let tok = toJWT(cfg, session) let tok = toJWT(cfg, session)
check fromJWT(cfg, tok) == session
check: test "ping":
fromJWT(cfg, tok) == session
test "can ping":
let resp = http.get(apiBase & "/ping") let resp = http.get(apiBase & "/ping")
check: check:
resp.status.startsWith("200") resp.status.startsWith("200")
resp.body == "\"pong\"" resp.body == "\"pong\""
test "can fail auth": test "fail auth":
let resp = http.get(apiBase & "/auth-token?username=bob@builder.com&password=notpassword") let resp = http.get(apiBase & "/auth-token?username=bob@builder.com&password=notpassword")
check: check resp.status.startsWith("401")
resp.status.startsWith("401")
test "can auth": test "auth":
let resp = http.get(apiBase & "/auth-token?username=bob@builder.com&password=password") let resp = http.get(apiBase & "/auth-token?username=bob@builder.com&password=password")
check: check resp.status.startsWith("200")
resp.status.startsWith("200")
test "verify valid auth token":
let authHttp = newAuthenticatedHttpClient(apiBase, "bob@builder.com", "password")
let resp = authHttp.get(apiBase & "/verify-auth")
check resp.status.startsWith("200")
test "verify fails when no auth token is given":
let resp = http.get(apiBase & "/verify-auth")
check resp.status.startsWith("401")
test "verify fails when invalid auth token is given":
let http1 = newHttpClient()
http1.headers = newHttpHeaders({"Authorization": "Bearer nope"})
let resp = http1.get(apiBase & "/verify-auth")
check resp.status.startsWith("401")
test "fail to get projects when not authenticated":
let resp = http.get(apiBase & "/projects")
check resp.status.startsWith("401")
test "get projects":
let authHttp = newAuthenticatedHttpClient(apiBase, "bob@builder.com", "password")
let resp = authHttp.get(apiBase & "/projects")
check resp.status.startsWith("200")
let projects: seq[ProjectDef] = parseJson(resp.body).getElems.mapIt(parseProjectDef(it))
check sameContents(projects, cfg.projects)
# suite tear-down # suite tear-down
try: discard http.post(apiBase & "/service/debug/stop") try: discard http.post(apiBase & "/service/debug/stop")