Added ProjectDef parsing code. Unit test for , authentication logic.
This commit is contained in:
parent
053ac8dc14
commit
ec967ec2bf
@ -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)
|
||||||
|
@ -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?":
|
||||||
|
@ -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"),
|
||||||
|
@ -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")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user