WIP Adding session auth and routes.
This commit is contained in:
		@@ -43,12 +43,11 @@ are:
 | 
				
			|||||||
* `artifactsRepo`: A string denoting the path to the artifacts repository
 | 
					* `artifactsRepo`: A string denoting the path to the artifacts repository
 | 
				
			||||||
  directory.
 | 
					  directory.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* `authSecret`: Secret key used to sign JWT session tokens.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* `users`: the array of user definition objects. Each user object is required
 | 
					* `users`: the array of user definition objects. Each user object is required
 | 
				
			||||||
  to have `username` and `hashedPwd` keys, both string.
 | 
					  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).
 | 
					* `projects`: an array of project definitions (detailed below).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
All are required.
 | 
					All are required.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								api.rst
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								api.rst
									
									
									
									
									
								
							@@ -1,9 +1,9 @@
 | 
				
			|||||||
GET    /api/ping
 | 
					✓ GET    /api/ping
 | 
				
			||||||
POST   /api/auth-token
 | 
					- POST   /api/auth-token
 | 
				
			||||||
GET    /api/projects                              -- return project summaries
 | 
					✓ GET    /api/projects                              -- return project summaries
 | 
				
			||||||
POST   /api/projects                              -- create a new project
 | 
					- POST   /api/projects                              -- create a new project
 | 
				
			||||||
GET    /api/project/<proj-id>                     -- return detailed project record (include steps)
 | 
					- GET    /api/project/<proj-id>                     -- return detailed project record (include steps)
 | 
				
			||||||
GET    /api/project/<proj-id>/<step-id>           -- return detailed step information (include runs)
 | 
					- GET    /api/project/<proj-id>/active              -- return detailed information about all currently running runs
 | 
				
			||||||
POST   /api/project/<proj-id>/<step-id>/run/<ref> -- kick off a run
 | 
					- GET    /api/project/<proj-id>/<step-id>           -- return detailed step information (include runs)
 | 
				
			||||||
GET    /api/project/<proj-id>/<step-id>/run/<ref> -- return detailed run information
 | 
					- POST   /api/project/<proj-id>/<step-id>/run/<ref> -- kick off a run
 | 
				
			||||||
 | 
					- GET    /api/project/<proj-id>/<step-id>/run/<ref> -- return detailed run information
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import logging, json, os, nre, sequtils, strtabs, tables
 | 
					import logging, json, os, nre, sequtils, strtabs, tables, times
 | 
				
			||||||
import private/util
 | 
					import private/util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Types
 | 
					# Types
 | 
				
			||||||
@@ -21,14 +21,23 @@ type
 | 
				
			|||||||
    cfgFilePath*, defaultBranch*, name*, repo*: string
 | 
					    cfgFilePath*, defaultBranch*, name*, repo*: string
 | 
				
			||||||
    envVars*: StringTableRef
 | 
					    envVars*: StringTableRef
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  StrawBossConfig* = object
 | 
					 | 
				
			||||||
    artifactsRepo*: string
 | 
					 | 
				
			||||||
    projects*: seq[ProjectDef]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  RunRequest* = object
 | 
					  RunRequest* = object
 | 
				
			||||||
    projectName*, stepName*, buildRef*, workspaceDir*: string
 | 
					    projectName*, stepName*, buildRef*, workspaceDir*: string
 | 
				
			||||||
    forceRebuild*: bool
 | 
					    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
 | 
					# internal utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let nullNode = newJNull()
 | 
					let nullNode = newJNull()
 | 
				
			||||||
@@ -62,9 +71,18 @@ proc loadStrawBossConfig*(cfgFile: string): StrawBossConfig =
 | 
				
			|||||||
        envVars: envVars,
 | 
					        envVars: envVars,
 | 
				
			||||||
        repo: pJson.getOrFail("repo", "project definition").getStr))
 | 
					        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(
 | 
					  result = StrawBossConfig(
 | 
				
			||||||
    artifactsRepo: jsonCfg.getIfExists("artifactsRepo").getStr("artifacts"),
 | 
					    artifactsRepo: jsonCfg.getIfExists("artifactsRepo").getStr("artifacts"),
 | 
				
			||||||
    projects: projectDefs)
 | 
					    authSecret: jsonCfg.getOrFail("authSecret", "strawboss config").getStr,
 | 
				
			||||||
 | 
					    projects: projectDefs,
 | 
				
			||||||
 | 
					    users: users)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc loadProjectConfig*(cfgFile: string): ProjectCfg =
 | 
					proc loadProjectConfig*(cfgFile: string): ProjectCfg =
 | 
				
			||||||
  if not existsFile(cfgFile):
 | 
					  if not existsFile(cfgFile):
 | 
				
			||||||
@@ -120,6 +138,15 @@ proc `%`*(s: BuildStatus): JsonNode =
 | 
				
			|||||||
    "details": s.details
 | 
					    "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 =
 | 
					proc `%`*(req: RunRequest): JsonNode =
 | 
				
			||||||
  result = %* {
 | 
					  result = %* {
 | 
				
			||||||
    "projectName": req.projectName,
 | 
					    "projectName": req.projectName,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import asyncdispatch, jester, json, osproc, tempfile
 | 
					import asyncdispatch, jester, json, jwt, osproc, sequtils, tempfile, times
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ./configuration, ./core, private/util
 | 
					import ./configuration, ./core, private/util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,6 +9,60 @@ type Worker = object
 | 
				
			|||||||
  process*: Process
 | 
					  process*: Process
 | 
				
			||||||
  workingDir*: string
 | 
					  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 =
 | 
					proc spawnWorker(req: RunRequest): Worker =
 | 
				
			||||||
  let dir = mkdtemp()
 | 
					  let dir = mkdtemp()
 | 
				
			||||||
  var args = @["run", req.projectName, req.stepName, "-r", req.buildRef, "-w", dir]
 | 
					  var args = @["run", req.projectName, req.stepName, "-r", req.buildRef, "-w", dir]
 | 
				
			||||||
@@ -25,8 +79,13 @@ proc start*(givenCfg: StrawBossConfig): void =
 | 
				
			|||||||
    get "/api/ping":
 | 
					    get "/api/ping":
 | 
				
			||||||
      resp $(%*"pong"), "application/json"
 | 
					      resp $(%*"pong"), "application/json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get "/api/auth-token":
 | 
				
			||||||
 | 
					      resp Http501, makeJsonResp(Http501), "application/json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    get "/api/projects":
 | 
					    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?":
 | 
					    post "/api/project/@projectName/@stepName/run/@buildRef?":
 | 
				
			||||||
      workers.add(spawnWorker(RunRequest(
 | 
					      workers.add(spawnWorker(RunRequest(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "artifactsRepo": "artifacts",
 | 
					  "artifactsRepo": "artifacts",
 | 
				
			||||||
  "users": [],
 | 
					  "users": [],
 | 
				
			||||||
  "tokens": [],
 | 
					  "authSecret": "change me",
 | 
				
			||||||
  "projects": [
 | 
					  "projects": [
 | 
				
			||||||
    { "name": "new-life-intro-band",
 | 
					    { "name": "new-life-intro-band",
 | 
				
			||||||
      "repo": "/home/jdb/projects/new-life-introductory-band" },
 | 
					      "repo": "/home/jdb/projects/new-life-introductory-band" },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,4 +10,5 @@ srcDir        = "src/main/nim"
 | 
				
			|||||||
# Dependencies
 | 
					# Dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
requires @["nim >= 0.16.1", "docopt >= 0.1.0", "tempfile", "jester"]
 | 
					requires @["nim >= 0.16.1", "docopt >= 0.1.0", "tempfile", "jester"]
 | 
				
			||||||
 | 
					requires "https://github.com/yglukhov/nim-jwt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user