diff --git a/core/src/main/resources/db/migration/V2_1__alter_release_id.sql b/core/src/main/resources/db/migration/V2_1__alter_release_id.sql new file mode 100644 index 00000000..47c5435b --- /dev/null +++ b/core/src/main/resources/db/migration/V2_1__alter_release_id.sql @@ -0,0 +1,17 @@ +--: ---------------------------------------------------------------------------- +--: Copyright (C) 2017 Verizon. All Rights Reserved. +--: +--: Licensed under the Apache License, Version 2.0 (the "License"); +--: you may not use this file except in compliance with the License. +--: You may obtain a copy of the License at +--: +--: http://www.apache.org/licenses/LICENSE-2.0 +--: +--: Unless required by applicable law or agreed to in writing, software +--: distributed under the License is distributed on an "AS IS" BASIS, +--: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +--: See the License for the specific language governing permissions and +--: limitations under the License. +--: +--: ---------------------------------------------------------------------------- +ALTER TABLE PUBLIC."audit_log" ALTER COLUMN "release_id" VARCHAR(25); \ No newline at end of file diff --git a/core/src/main/scala/AccessToken.scala b/core/src/main/scala/AccessToken.scala index e78b909f..5658a1f4 100644 --- a/core/src/main/scala/AccessToken.scala +++ b/core/src/main/scala/AccessToken.scala @@ -17,5 +17,6 @@ package nelson final case class AccessToken( - value: String + value: String, + isPrivate: Boolean = false ) diff --git a/core/src/main/scala/Config.scala b/core/src/main/scala/Config.scala index 90732fb4..d6d3473d 100644 --- a/core/src/main/scala/Config.scala +++ b/core/src/main/scala/Config.scala @@ -18,18 +18,17 @@ package nelson import java.nio.file.{Path, Paths} import java.util.concurrent.{ExecutorService, Executors, ScheduledExecutorService, ThreadFactory} - import journal.Logger import nelson.BannedClientsConfig.HttpUserAgent import nelson.cleanup.ExpirationPolicy +import nelson.Github.GithubOp import org.http4s.Uri import org.http4s.client.Client import org.http4s.client.blaze._ import storage.StoreOp -import logging.{WorkflowLogger,LoggingOp} -import audit.{Auditor,AuditEvent} -import notifications.{SlackHttp,SlackOp,EmailOp,EmailServer} - +import logging.{LoggingOp, WorkflowLogger} +import audit.{AuditEvent, Auditor} +import notifications.{EmailOp, EmailServer, SlackHttp, SlackOp} import scala.concurrent.ExecutionContext import scala.concurrent.duration._ import scalaz.Scalaz._ @@ -40,57 +39,139 @@ import scheduler.SchedulerOp import vault._ import vault.http4s._ -/** - * - */ -final case class GithubConfig( - domain: Option[String], - clientId: String, - clientSecret: String, - redirectUri: String, - scope: String, - systemAccessToken: AccessToken, - systemUsername: String, - organizationBlacklist: List[String], - organizationAdminList: List[String] -){ +sealed abstract class ScmConfig extends Product with Serializable { + def domain: Option[String] + def clientId: String + def clientSecret: String + def redirectUri: String + def scope: String + def systemAccessToken: AccessToken + def systemUsername: String + def organizationBlacklist: List[String] + def organizationAdminList: List[String] def isEnterprise: Boolean = domain.nonEmpty + def base: String + def oauth: String + def api: String + def tokenEndpoint: String + def loginEndpoint: String + def userEndpoint: String + def userOrgsEndpoint: String + def orgEndpoint(login: String): String + def repoEndpoint(page: Int = 1): String + def webhookEndpoint(slug: Slug): String + def contentsEndpoint(slug: Slug, path: String): String + def releaseEndpoint(slug: Slug, releaseId: String): String + + def withOrganizationAdminList(l: List[String]): ScmConfig + + private [nelson] def encodeURI(uri: String): String = + java.net.URLEncoder.encode(uri, "UTF-8") +} +object ScmConfig { + final case class GithubConfig( + domain: Option[String], + clientId: String, + clientSecret: String, + redirectUri: String, + scope: String, + systemAccessToken: AccessToken, + systemUsername: String, + organizationBlacklist: List[String], + organizationAdminList: List[String]) extends ScmConfig { - val oauth = - "https://"+ domain.fold("github.com")(identity) + val base: String = + "https://"+ domain.fold("github.com")(identity) - val api = - "https://"+ domain.fold("api.github.com")(_+"/api/v3") + val oauth: String = + base - val tokenEndpoint = - s"${oauth}/login/oauth/access_token" + val api: String = + "https://"+ domain.fold("api.github.com")(_+"/api/v3") - val loginEndpoint = - s"${oauth}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURI(redirectUri)}&scope=${scope}" + val tokenEndpoint: String = + s"$oauth/login/oauth/access_token" - val userEndpoint = - s"${api}/user" + val loginEndpoint: String = + s"$oauth/login/oauth/authorize?client_id=$clientId&redirect_uri=${encodeURI(redirectUri)}&scope=$scope" - val userOrgsEndpoint = - s"${userEndpoint}/orgs" + val userEndpoint: String = + s"$api/user" - def orgEndpoint(login: String) = - s"${api}/orgs/${login}" + val userOrgsEndpoint: String = + s"$userEndpoint/orgs" - def repoEndpoint(page: Int = 1) = - s"${api}/user/repos?affiliation=owner,organization_member&visibility=all&direction=asc&page=${page}" + def orgEndpoint(login: String): String = + s"$api/orgs/$login" - def webhookEndpoint(slug: Slug) = - s"${api}/repos/${slug}/hooks" + def repoEndpoint(page: Int = 1): String = + s"$api/user/repos?affiliation=owner,organization_member&visibility=all&direction=asc&page=$page" - def contentsEndpoint(slug: Slug, path: String) = - s"${api}/repos/${slug}/contents/${path}" + def webhookEndpoint(slug: Slug): String = + s"$api/repos/$slug/hooks" - def releaseEndpoint(slug: Slug, releaseId: Long) = - s"${api}/repos/${slug}/releases/${releaseId}" + def contentsEndpoint(slug: Slug, path: String): String = + s"$api/repos/$slug/contents/$path" - private [nelson] def encodeURI(uri: String): String = - java.net.URLEncoder.encode(uri, "UTF-8") + def releaseEndpoint(slug: Slug, releaseId: String): String = + s"$api/repos/$slug/releases/$releaseId" + + def withOrganizationAdminList(l: List[String]): ScmConfig = + this.copy(organizationAdminList = l) + } + + final case class GitlabConfig( + domain: Option[String], + clientId: String, + clientSecret: String, + redirectUri: String, + scope: String, + systemAccessToken: AccessToken, + systemUsername: String, + organizationBlacklist: List[String], + organizationAdminList: List[String]) extends ScmConfig { + + val base: String = + "https://" + domain.fold("gitlab.com")(identity) + + val oauth: String = + base + "/oauth" + + val api: String = + base + "/api/v4" + + val tokenEndpoint: String = + s"$oauth/token" + + val loginEndpoint: String = + s"$oauth/authorize?client_id=$clientId&redirect_uri=${encodeURI(redirectUri)}&response_type=code" + + val userEndpoint: String = + s"$api/user" + + val userOrgsEndpoint: String = + s"$api/groups" + + def orgEndpoint(login: String): String = + s"$api/groups/$login" + + def repoEndpoint(page: Int = 1): String = + s"$api/projects?page=$page" + + def webhookEndpoint(slug: Slug): String = + s"$api/projects/${ urlEncode(slug.toString) }/hooks" + + def contentsEndpoint(slug: Slug, path: String): String = + s"$api/projects/${ urlEncode(slug.toString) }/repository/files/$path" + + def releaseEndpoint(slug: Slug, releaseId: String): String = + s"$api/projects/${ urlEncode(slug.toString) }/repository/tags/$releaseId" + + def withOrganizationAdminList(l: List[String]): ScmConfig = + this.copy(organizationAdminList = l) + + private[this] def urlEncode(s: String) = java.net.URLEncoder.encode(s, "UTF-8") + } } /** @@ -311,7 +392,7 @@ final case class PolicyConfig( * actually cares about. */ final case class NelsonConfig( - git: GithubConfig, + git: ScmConfig, network: NetworkConfig, security: SecurityConfig, database: DatabaseConfig, @@ -395,7 +476,12 @@ object Config { val nomadcfg = readNomad(cfg.subconfig("nelson.nomad")) val gitcfg = readGithub(cfg.subconfig("nelson.github")) - val git = new Github.GithubHttp(gitcfg, http0) + val git: GithubOp ~> Task = gitcfg match { + case _: ScmConfig.GithubConfig => + new Github.GithubHttp(gitcfg, http0) + case _ => + new Gitlab.GitlabHttp(gitcfg, http4sClient(timeout)) + } val workflowConf = readWorkflowLogger(cfg.subconfig("nelson.workflow-logger")) val workflowlogger = new WorkflowLogger( @@ -686,18 +772,24 @@ object Config { monitoringPort = cfg.require[Int]("monitoring-port") ) - private def readGithub(cfg: KConfig): GithubConfig = - GithubConfig( - domain = cfg.lookup[String]("domain"), - clientId = cfg.require[String]("client-id"), - clientSecret = cfg.require[String]("client-secret"), - redirectUri = cfg.require[String]("redirect-uri"), - scope = cfg.require[String]("scope"), - systemAccessToken = AccessToken(cfg.require[String]("access-token")), - systemUsername = cfg.require[String]("system-username"), - organizationBlacklist = cfg.lookup[List[String]]("organization-blacklist").getOrElse(Nil), - organizationAdminList = cfg.lookup[List[String]]("organization-admins").getOrElse(Nil) + private def readGithub(cfg: KConfig): ScmConfig = { + val service = cfg.lookup[String]("service").getOrElse("github") + val attrs = ( + cfg.lookup[String]("domain"), + cfg.require[String]("client-id"), + cfg.require[String]("client-secret"), + cfg.require[String]("redirect-uri"), + cfg.require[String]("scope"), + AccessToken(cfg.require[String]("access-token"), isPrivate = true), + cfg.require[String]("system-username"), + cfg.lookup[List[String]]("organization-blacklist").getOrElse(Nil), + cfg.lookup[List[String]]("organization-admins").getOrElse(Nil) ) + import ScmConfig._ + val confBuilder = + if (service == "github") GithubConfig else GitlabConfig + confBuilder.tupled(attrs) + } private def readSlack(cfg: KConfig): Option[SlackConfig] = { for { diff --git a/core/src/main/scala/Github.scala b/core/src/main/scala/Github.scala index 08f6b06f..1c4594ae 100644 --- a/core/src/main/scala/Github.scala +++ b/core/src/main/scala/Github.scala @@ -76,13 +76,13 @@ object Github { ) extends Event final case class ReleaseEvent( - id: Long, + id: String, slug: Slug, repositoryId: Long ) extends Event final case class Release( - id: Long, + id: String, url: String, htmlUrl: String, assets: Seq[Asset], @@ -129,7 +129,7 @@ object Github { final case class GetReleaseAssetContent(asset: Github.Asset, t: AccessToken) extends GithubOp[Github.Asset] - final case class GetRelease(slug: Slug, releaseId: ID, t: AccessToken) + final case class GetRelease(slug: Slug, releaseId: String, t: AccessToken) extends GithubOp[Github.Release] final case class GetUserRepositories(token: AccessToken) @@ -178,7 +178,7 @@ object Github { * nelson gets notified of a release, as the payload we get does not contain * the assets that we need. */ - def fetchRelease(slug: Slug, id: ID)(t: AccessToken): GithubOpF[Github.Release] = + def fetchRelease(slug: Slug, id: String)(t: AccessToken): GithubOpF[Github.Release] = for { r <- Free.liftFC(GetRelease(slug, id, t)) a <- fetchReleaseAssets(r)(t) @@ -226,7 +226,7 @@ object Github { } } - final class GithubHttp(cfg: GithubConfig, http: dispatch.Http) extends (GithubOp ~> Task) { + final class GithubHttp(cfg: ScmConfig, http: dispatch.Http) extends (GithubOp ~> Task) { import java.net.URI import dispatch._, Defaults._ import nelson.Json._ @@ -272,7 +272,7 @@ object Github { b <- http(url(a) OK as.String).toTask } yield asset.copy(content = Option(b)) - case GetRelease(slug: Slug, releaseId: ID, t: AccessToken) => + case GetRelease(slug: Slug, releaseId: String, t: AccessToken) => for { resp <- fetch(cfg.releaseEndpoint(slug, releaseId), t) rel <- fromJson[Github.Release](resp) diff --git a/core/src/main/scala/Gitlab.scala b/core/src/main/scala/Gitlab.scala new file mode 100644 index 00000000..ff621d59 --- /dev/null +++ b/core/src/main/scala/Gitlab.scala @@ -0,0 +1,380 @@ +//: ---------------------------------------------------------------------------- +//: Copyright (C) 2017 Verizon. All Rights Reserved. +//: +//: Licensed under the Apache License, Version 2.0 (the "License"); +//: you may not use this file except in compliance with the License. +//: You may obtain a copy of the License at +//: +//: http://www.apache.org/licenses/LICENSE-2.0 +//: +//: Unless required by applicable law or agreed to in writing, software +//: distributed under the License is distributed on an "AS IS" BASIS, +//: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//: See the License for the specific language governing permissions and +//: limitations under the License. +//: +//: ---------------------------------------------------------------------------- +package nelson + +import argonaut.{CodecJson, DecodeJson, DecodeResult, EncodeJson} +import argonaut.Argonaut.{casecodec2, casecodec4, jencode1L} +import nelson.Github._ +import org.http4s, http4s._ +import org.http4s.Status.Successful +import org.http4s.argonaut.jsonOf +import org.http4s.client.{Client, UnexpectedStatus} +import org.http4s.headers.{Accept, Authorization, MediaRangeAndQValue} +import org.http4s.util.CaseInsensitiveString +import scalaz._, Scalaz._ +import scalaz.concurrent.Task +import scala.collection.mutable.ListBuffer +import scala.util.Try +import java.net.{MalformedURLException, URI} + +object Gitlab { + + final class GitlabHttp(cfg: ScmConfig, http4sClient: Client) extends (GithubOp ~> Task) { + def apply[A](in: GithubOp[A]): Task[A] = in match { + case GetAccessToken(fromCode: String) => + fetch[AccessToken](cfg.tokenEndpoint) { uri => + http4s.Request( + Method.POST, + uri + .withQueryParam("client_id", cfg.clientId) + .withQueryParam("client_secret", cfg.clientSecret) + .withQueryParam("code", fromCode) + .withQueryParam("grant_type", "authorization_code") + .withQueryParam("redirect_uri", cfg.redirectUri) + ) + } + + case GetUser(token: AccessToken) => + authFetch[Github.User](cfg.userEndpoint, token) + + case GetUserOrgKeys(token: AccessToken) => + authFetch[List[Github.OrgKey]](cfg.userOrgsEndpoint, token) + + case GetOrganizations(keys: List[Github.OrgKey], t: AccessToken) => + Nondeterminism[Task].gatherUnordered( + keys.map(key => authFetch[Organization](cfg.orgEndpoint(key.slug), t)) + ) + + case GetUserRepositories(t: AccessToken) => + def go(uri: String)(accum: List[Repo]): Task[List[Repo]] = { + authFetchWithMeta[List[Repo]](uri, t).flatMap { case EntityWithMeta(repos, pagination) => + val total = accum ++ repos + pagination.nextPage.fold(Task.now(total)) { page => + val nextLink = pagination.links.getOrElse("next", cfg.repoEndpoint(page)) + go(nextLink)(total) + } + } + } + go(cfg.repoEndpoint(page = 1))(Nil) + + case GetFileFromRepository(slug: Slug, path: String, tagOrBranch: String, t: AccessToken) => + authFetch[Github.Contents]( + cfg.contentsEndpoint(slug, java.net.URLEncoder.encode(path, "UTF-8")), + t, + req => req.withUri(req.uri.withQueryParam("ref", tagOrBranch)) + ).map(Option.apply).or(Task.now(Option.empty)) + + case GetRepoWebHooks(slug: Slug, t: AccessToken) => + authFetch[List[GitlabWebhook]](cfg.webhookEndpoint(slug), t).map(_.map(_.toGithubWebHook)) + + case PostRepoWebHook(slug: Slug, hook: Github.WebHook, t: AccessToken) => + def eventsParams = { + val available = + List( + "push_events", + "issues_events", + "merge_requests_events", + "tag_push_events", + "note_events", + "job_events", + "pipeline_events", + "wiki_page_events" + ) + (available.filter(hook.events.contains).map(_ -> List(true)) ++ + available.filter(!hook.events.contains(_)).map(_ -> List(false))).toMap + } + authFetch[GitlabWebhook](cfg.webhookEndpoint(slug), t, + req => + req.withMethod(Method.POST).withUri(req.uri + .setQueryParams(eventsParams) + .withQueryParam("id", hook.id) + .withQueryParam("url", hook.config.getOrElse("url", "")) + ) + ).map(_.toGithubWebHook) + + case DeleteRepoWebHook(slug: Slug, id: Long, t: AccessToken) => + Uri.fromString(s"${ cfg.webhookEndpoint(slug) }/$id") + .fold( + t => Task.fail(new MalformedURLException(t.message)), uri => + http4sClient.successful( + http4s.Request(Method.DELETE, uri = uri, headers = Headers(authHeader(t))) + ) + ) + .map(_ => ()) + .handle { + // swallow 404 (NotFound) as valid response + case UnexpectedStatus(Status(404)) => () + } + + case GetRelease(slug: Slug, releaseId: String, t: AccessToken) => + def urlEncode(s: String) = java.net.URLEncoder.encode(s, "UTF-8") + authFetch[Tag](cfg.releaseEndpoint(slug, releaseId), t).map { tag => + val baseUrl = s"${ cfg.base }/${ urlEncode(slug.owner) }/${ urlEncode(slug.repository) }" + val tagUrl = s"$baseUrl/tags/${ urlEncode(tag.name) }" + Release(tag.name, tagUrl, tagUrl, tag.assets(baseUrl), tag.name) + }.flatMap { release => + if (release.assets.isEmpty) + Task.fail(new IllegalArgumentException( + s"The tag '$releaseId' for repo '$slug' is not a valid release. " + + "Have you added an asset as release description?." + )) + else + Task.now(release) + } + + case GetReleaseAssetContent(asset: Github.Asset, t: AccessToken) => + Uri.fromString(asset.url) + .fold(t => Task.fail(new MalformedURLException(t.message)), uri => + http4sClient.fetch( + http4s.Request(uri = uri, headers = Headers(authHeader(t))) + )(EntityDecoder.decodeString) + ).map(body => asset.copy(content = Option(body))) + } + + private[this] def authFetch[A: EntityDecoder]( + uri: String, + token: AccessToken, + f: Request => Request = identity + ): Task[A] = + authFetchWithMeta(uri, token, f).map(_.entity) + + private[this] def authHeader(token: AccessToken): Header = + if (token.isPrivate) + Header("PRIVATE-TOKEN", token.value) + else + Authorization(Credentials.Token(AuthScheme.Bearer, token.value)) + + private[this] def authFetchWithMeta[A: EntityDecoder]( + uri: String, + token: AccessToken, + f: Request => Request = identity + ): Task[EntityWithMeta[A]] = + fetchWithMeta[A](uri) { uri => + f(http4s.Request(headers = Headers(authHeader(token)), uri = uri)) + } + + private[this] def fetch[A: EntityDecoder](uri: String)(f: Uri => Request): Task[A] = + fetchWithMeta(uri)(f).map(_.entity) + + private[this] def fetchWithMeta[A](uri: String)(f: Uri => Request)(implicit d: EntityDecoder[A]): Task[EntityWithMeta[A]] = + Uri.fromString(uri).map { uri => + if (d.consumes.nonEmpty) { + val m = d.consumes.toList + f(uri).putHeaders(Accept(MediaRangeAndQValue(m.head), m.tail.map(MediaRangeAndQValue(_)):_*)) + } else f(uri) + }.fold( + t => Task.fail(new MalformedURLException(t.message)), + req => http4sClient.fetch(req) { + case Successful(resp) => + val pagination = resp.headers.toPagination + d.decode(resp, strict = false).fold(throw _, entity => EntityWithMeta(entity, pagination)) + case failedResponse => + Task.fail(UnexpectedStatus(failedResponse.status)) + } + ) + } + + final case class EntityWithMeta[A]( + entity: A, + pagination: Pagination + ) + + final case class Pagination( + totalItems: Long, + totalPages: Int, + itemsPerPage: Int, + currentPage: Int, + nextPage: Option[Int], + prevPage: Option[Int], + links: Map[String, String] + ) + + implicit class HeadersOps(hs: Headers) { + def toPagination: Pagination = + Pagination( + totalItems = getAsLong("X-Total"), + totalPages = getAsInt("X-Total-Pages"), + itemsPerPage = getAsInt("X-Per-Page"), + currentPage = getAsInt("X-Page"), + nextPage = get("X-Next-Page").map(_.toInt), + prevPage = get("X-Prev-Page").map(_.toInt), + links = get("Link").map(buildLinksMap).getOrElse(Map.empty) + ) + + private[this] def getAsInt(label: String): Int = + get(label).map(_.toInt).getOrElse(0) + + private[this] def getAsLong(label: String): Long = + get(label).map(_.toLong).getOrElse(0L) + + private[this] def get(label: String): Option[String] = + hs.get(CaseInsensitiveString(label)).map(_.value).flatMap(toOption) + + private[this] def buildLinksMap(str: String): Map[String, String] = + str.split(',') + .map(_.split(';')) + .map(_.map(_.trim)) + .map(pair => pair.last.split("\"")(1) -> pair.head.substring(1, pair.head.length - 1)) + .toMap + + private[this] def toOption(str: String): Option[String] = + if (str == null || str.isEmpty) None else Option(str) + } + + implicit def genericEntityDecoder[A: DecodeJson]: EntityDecoder[A] = jsonOf[A] + + implicit lazy val GithubUser: CodecJson[Github.User] = + casecodec4(Github.User.apply, Github.User.unapply + )("username", "avatar_url", "name", "email") + + implicit lazy val GithubOrg: CodecJson[Github.OrgKey] = + casecodec2(Github.OrgKey.apply, Github.OrgKey.unapply)("id", "full_path") + + implicit val GithubContentsDecoder: DecodeJson[Github.Contents] = + DecodeJson(c => + ((c --\ "content").as[String] |@| + (c --\ "file_name").as[String] |@| + (c --\ "size").as[Long] + )(Github.Contents.apply) + ) + + implicit lazy val OrganizationDecoder: DecodeJson[Organization] = + DecodeJson(c => + ((c --\ "id").as[Long] |@| + (c --\ "name").as[Option[String]] |@| + (c --\ "full_path").as[String] |@| + (c --\ "avatar_url").as[Option[URI]] + ) { case (id, name, login, avatar) => Organization(id, name, login, avatar) } + ) + + case class GitlabPermissions(projectAccess: Option[RepoAccess], groupAccess: Option[RepoAccess]) + + implicit lazy val GitlabPermissionsDecoder: DecodeJson[GitlabPermissions] = + DecodeJson(c => for { + pa <- (c --\ "project_access").as[Option[RepoAccess]] + ga <- (c --\ "group_access").as[Option[RepoAccess]] + } yield GitlabPermissions(pa, ga) + ) + + implicit lazy val RepoDecoder: DecodeJson[Repo] = + DecodeJson(c => for { + z <- (c --\ "id").as[Long] + y <- (c --\ "path_with_namespace").as[String] + x <- Slug.fromString(y).map(DecodeResult.ok).valueOr(e => DecodeResult.fail(e.getMessage,c.history)) + w <- (c --\ "permissions").as[GitlabPermissions].map { + case GitlabPermissions(Some(pa), _) => pa + case GitlabPermissions(_, Some(ga)) => ga + case _ => RepoAccess.Forbidden + } + } yield Repo(z, x, w)) + + implicit lazy val RepoAccessDecoder: DecodeJson[Option[RepoAccess]] = + DecodeJson(c => + (c --\ "access_level").as[Option[Int]].map(_.map(RepoAccess.fromInt)) + ) + + implicit lazy val AccessTokenDecoder: DecodeJson[AccessToken] = + DecodeJson(c => (c --\ "access_token").as[String]).map(AccessToken.apply(_)) + + implicit lazy val AccessTokenEncoder: EncodeJson[AccessToken] = + jencode1L((t: AccessToken) => t.value)("access_token") + + case class GitlabWebhook( + id: Long, + url: String, + pushEvents: Boolean, + tagPushEvents: Boolean, + repositoryUpdateEvents: Boolean, + issuesEvents: Boolean, + mergeRequestEvents: Boolean, + noteEvents: Boolean, + pipelineEvents: Boolean, + wikiPageEvents: Boolean, + jobEvents: Boolean + ) + + implicit class GitlabWebhookOps(instance: GitlabWebhook) { + def toGithubWebHook: Github.WebHook = { + def eventsList: List[String] = { + import instance._ + val buffer = new ListBuffer[String]() + if (pushEvents) buffer += "push_events" + if (tagPushEvents) buffer += "tag_push_events" + if (repositoryUpdateEvents) buffer += "repository_update_events" + if (issuesEvents) buffer += "issues_events" + if (mergeRequestEvents) buffer += "merge_requests_events" + if (noteEvents) buffer += "note_events" + if (pipelineEvents) buffer += "pipeline_events" + if (wikiPageEvents) buffer += "wiki_page_events" + if (jobEvents) buffer += "job_events" + buffer.toList + } + Github.WebHook( + instance.id, + instance.id.toString, + eventsList, + active = true, + config = Map("url" -> instance.url, "content_type" -> "json") + ) + } + } + + implicit lazy val GitlabWebhookDecoder: DecodeJson[GitlabWebhook] = + DecodeJson(c => + ((c --\ "id").as[Long] |@| + (c --\ "url").as[String] |@| + (c --\ "push_events").as[Boolean] |@| + (c --\ "tag_push_events").as[Boolean] |@| + (c --\ "repository_update_events").as[Boolean] |@| + (c --\ "issues_events").as[Boolean] |@| + (c --\ "merge_requests_events").as[Boolean] |@| + (c --\ "note_events").as[Boolean] |@| + (c --\ "pipeline_events").as[Boolean] |@| + (c --\ "wiki_page_events").as[Boolean] |@| + (c --\ "job_events").as[Boolean] + )(GitlabWebhook.apply) + ) + + final case class Tag( + name: String, + releaseDescription: Option[String] + ) + + implicit class TagOps(tag: Tag) { + def assets(baseUrl: String): Seq[Asset] = + tag.releaseDescription.flatMap { desc => + Try { + val pattern = """\[(.*)\]\((.*)\)""".r + val pattern(assetName, assetRelUrl) = desc + Github.Asset(0L, assetName, "", s"$baseUrl$assetRelUrl") + }.toOption + }.toSeq + } + + implicit val GitlabTagDecoder: DecodeJson[Tag] = + DecodeJson(z => + ((z --\ "name").as[String] |@| + (z --\ "release" --\ "description").as[Option[String]] + ) ((a, b) => Tag(a, b.map(_.trim))) + ) + + implicit lazy val UriToJson: EncodeJson[URI] = + implicitly[EncodeJson[String]].contramap(_.toString) + + implicit lazy val JsonToUri: DecodeJson[URI] = + implicitly[DecodeJson[String]].map(new URI(_)) +} \ No newline at end of file diff --git a/core/src/main/scala/Json.scala b/core/src/main/scala/Json.scala index 7a2f776f..057c715a 100644 --- a/core/src/main/scala/Json.scala +++ b/core/src/main/scala/Json.scala @@ -36,8 +36,11 @@ object Json { implicit lazy val DurationEncoder: EncodeJson[Duration] = implicitly[EncodeJson[Long]].contramap(_.toMillis) - implicit lazy val AccessTokenCodec: CodecJson[AccessToken] = - casecodec1(AccessToken.apply, AccessToken.unapply)("access_token") + implicit lazy val AccessTokenDecoder: DecodeJson[AccessToken] = + DecodeJson(c => (c --\ "access_token").as[String]).map(AccessToken.apply(_)) + + implicit lazy val AccessTokenEncoder: EncodeJson[AccessToken] = + jencode1L((t: AccessToken) => t.value)("access_token") implicit lazy val UserCodec: CodecJson[User] = casecodec5(User.apply, User.unapply)("login", "avatar_url", "name", "email", "organizations") @@ -158,7 +161,7 @@ object Json { ((c --\ "id").as[Long] |@| (c --\ "name").as[Option[String]] |@| (c --\ "login").as[String] |@| - (c --\ "avatar_url").as[URI] + (c --\ "avatar_url").as[Option[URI]] )(Organization.apply) ) @@ -255,6 +258,7 @@ object Json { */ implicit lazy val GithubEventDecoder: DecodeJson[Github.Event] = GithubReleaseEventDecoder.asInstanceOf[DecodeJson[Github.ReleaseEvent]] ||| + GitlabReleaseEventDecoder.asInstanceOf[DecodeJson[Github.ReleaseEvent]] ||| GithubPingEventDecoder.asInstanceOf[DecodeJson[Github.Event]] /** @@ -376,7 +380,7 @@ object Json { (z --\ "tag_name").as[String] ) ((a, b, c, d, e) => Github.Release( - id = a, + id = a.toString, url = b, htmlUrl = c, assets = d, @@ -397,7 +401,7 @@ object Json { implicit val GithubReleaseEncoder: EncodeJson[Github.Release] = EncodeJson((release: Github.Release) => - ("id" := release.id) ->: + ("id" := release.id.toLong) ->: ("url" := release.url) ->: ("html_url" := release.htmlUrl) ->: ("assets" := release.assets) ->: @@ -498,7 +502,7 @@ object Json { */ implicit val GithubReleaseEventDecoder: DecodeJson[Github.ReleaseEvent] = DecodeJson(z => for { - a <- (z --\ "release" --\ "id").as[Long] + a <- (z --\ "release" --\ "id").as[String] d <- (z --\ "repository" --\ "full_name").as[String] x <- Slug.fromString(d).map(DecodeResult.ok ).valueOr(e => DecodeResult.fail(e.getMessage,z.history)) @@ -511,6 +515,16 @@ object Json { ) }) + implicit val GitlabReleaseEventDecoder: DecodeJson[Github.ReleaseEvent] = + DecodeJson(z => for { + a <- (z --\ "ref").as[String] + i = a.split('/').last + p <- (z --\ "project" --\ "path_with_namespace").as[String] + x <- Slug.fromString(p).map(DecodeResult.ok + ).valueOr(e => DecodeResult.fail(e.getMessage,z.history)) + e <- (z --\ "project_id").as[Long] + } yield Github.ReleaseEvent(id = i, slug = x, repositoryId = e)) + /** * { * "url": "https://github.example.com/api/v3/repos/example/howdy/releases/assets/1", diff --git a/core/src/main/scala/Released.scala b/core/src/main/scala/Released.scala index 568c5a50..3bf6b3cb 100644 --- a/core/src/main/scala/Released.scala +++ b/core/src/main/scala/Released.scala @@ -30,7 +30,7 @@ final case class Released( /* when was this release created */ timestamp: Instant, /* reference id from github */ - releaseId: Long, + releaseId: String, /* informational URIs on github, to be used in a UI */ releaseHtmlUrl: URI ) @@ -52,8 +52,8 @@ final case class ReleasedDeployment( object Released { import scalaz.Order - import scalaz.std.anyVal._ + import scalaz.std.string._ implicit def releasedOrder: Order[Released] = - Order[Long].contramap[Released](_.releaseId) + Order[String].contramap[Released](_.releaseId) } diff --git a/core/src/main/scala/Repo.scala b/core/src/main/scala/Repo.scala index 8235aedc..7c94bcfb 100644 --- a/core/src/main/scala/Repo.scala +++ b/core/src/main/scala/Repo.scala @@ -98,4 +98,14 @@ object RepoAccess { case Some(a) => \/.right(a) case _ => \/.left(InvalidRepoAccess(s)) } + + def fromInt(i: Int): RepoAccess = + if (i >= 40) // Gitlab: Master (40) or Owner (50) + Admin + else if (i >= 30) // Gitlab: Developer (30) + Push + else if (i >= 10) // Gitlab: Reporter (20) or Guest (10) + Pull // TODO `Guest` only if the project is public or internal + else + Forbidden } diff --git a/core/src/main/scala/Session.scala b/core/src/main/scala/Session.scala index 1718cfe9..e41064cf 100644 --- a/core/src/main/scala/Session.scala +++ b/core/src/main/scala/Session.scala @@ -47,7 +47,7 @@ object Session { session => (uint32 :: optional(bool, nonGreedyString) :: nonGreedyString :: - uriCodec + optional(bool, uriCodec) ).as[Organization] val userCodec: Codec[User] = @@ -63,7 +63,7 @@ object Session { session => instant => (instant.getEpochSecond, instant.getNano)) val accessTokenCodec: Codec[AccessToken] = - (nonGreedyString).as[AccessToken] + (nonGreedyString :: bool(1)).as[AccessToken] def sessionCodecV3( authEnv: AuthEnv, diff --git a/core/src/main/scala/User.scala b/core/src/main/scala/User.scala index 41f4fc0b..a0561053 100644 --- a/core/src/main/scala/User.scala +++ b/core/src/main/scala/User.scala @@ -29,14 +29,14 @@ final case class User( orgs: List[Organization] ){ val toOrganization: Organization = - Organization(0l, Option(name), login, avatar) + Organization(0l, Option(name), login, Some(avatar)) } final case class Organization( id: Long, _name: Option[String], slug: String, - avatar: URI + avatar: Option[URI] ){ val name: String = _name.getOrElse(slug) diff --git a/core/src/main/scala/audit/AuditEvent.scala b/core/src/main/scala/audit/AuditEvent.scala index 0560fefb..b824fe2d 100644 --- a/core/src/main/scala/audit/AuditEvent.scala +++ b/core/src/main/scala/audit/AuditEvent.scala @@ -23,13 +23,13 @@ final case class AuditEvent[A]( event: A, timestamp: Instant, action: AuditAction, - releaseId: Option[Long], + releaseId: Option[String], userLogin: String, auditable: Auditable[A] ) object AuditEvent { - def apply[A](event: A, action: AuditAction, releaseId: Option[Long], userLogin: String)(implicit au: Auditable[A]): AuditEvent[A] = + def apply[A](event: A, action: AuditAction, releaseId: Option[String], userLogin: String)(implicit au: Auditable[A]): AuditEvent[A] = AuditEvent[A](event, Instant.now, action, releaseId, userLogin, au) } diff --git a/core/src/main/scala/audit/AuditLog.scala b/core/src/main/scala/audit/AuditLog.scala index 616fce23..67ac5630 100644 --- a/core/src/main/scala/audit/AuditLog.scala +++ b/core/src/main/scala/audit/AuditLog.scala @@ -21,7 +21,7 @@ package audit final case class AuditLog( id: ID, timestamp: java.time.Instant, - releaseId: Option[Long], + releaseId: Option[String], event: Option[argonaut.Json], category: String, action: String, diff --git a/core/src/main/scala/audit/Auditor.scala b/core/src/main/scala/audit/Auditor.scala index f90acc4e..db6ef4e1 100644 --- a/core/src/main/scala/audit/Auditor.scala +++ b/core/src/main/scala/audit/Auditor.scala @@ -51,7 +51,7 @@ class Auditor(queue: Queue[AuditEvent[_]], defaultLogin: String) { def errorSink: Sink[Task, Throwable] = sink.lift[Task,Throwable](t => Task.delay(logger.error(t.getMessage))) // possibly truncate - def write[A](a: A, action: AuditAction, releaseId: Option[Long] = None, login: String = defaultLogin)(implicit au: Auditable[A]): Task[Unit] = + def write[A](a: A, action: AuditAction, releaseId: Option[String] = None, login: String = defaultLogin)(implicit au: Auditable[A]): Task[Unit] = queue.enqueueOne(AuditEvent(a, action, releaseId, login)) def process(stg: (StoreOp ~> Task)): Process[Task, Unit] = diff --git a/core/src/main/scala/storage/h2.scala b/core/src/main/scala/storage/h2.scala index 22fbb511..31d36c9c 100644 --- a/core/src/main/scala/storage/h2.scala +++ b/core/src/main/scala/storage/h2.scala @@ -117,7 +117,7 @@ final case class H2Storage(xa: Transactor[Task]) extends (StoreOp ~> Task) { implicit val namespaceName: Meta[NamespaceName] = Meta[String].xmap({name => NamespaceName.fromString(name).toOption.yolo(s"metaNamespaceName: could not extract namespace name from $name")}, _.asString) - type AuditRow = (ID, Instant, Option[Long], String, String, String, Option[String]) + type AuditRow = (ID, Instant, Option[String], String, String, String, Option[String]) def listAuditLog( limit: Long, offset: Long, action: Option[String], category: Option[String] @@ -635,7 +635,7 @@ final case class H2Storage(xa: Transactor[Task]) extends (StoreOp ~> Task) { slug: Slug, version: Version, timestamp: Instant, - releaseId: Long, + releaseId: String, releaseHtmlUrl: URI, unitName: String, namespace: String, @@ -782,7 +782,7 @@ final case class H2Storage(xa: Transactor[Task]) extends (StoreOp ~> Task) { """.update.run.void } - def rowToReleased(row: (String,String,Instant,Long,String)): Option[Released] = + def rowToReleased(row: (String,String,Instant,String,String)): Option[Released] = (Slug.fromString(row._1).toOption |@| Version.fromString(row._2) )((a,b) => Released(a,b,row._3, row._4, new URI(row._5))) @@ -803,7 +803,7 @@ final case class H2Storage(xa: Transactor[Task]) extends (StoreOp ~> Task) { ON rr.id = r.repository_id WHERE u.name = $un AND u.version = ${v.toString} LIMIT 1 - """.query[(String,String,Instant,Long,String)] + """.query[(String,String,Instant,String,String)] query.map(rowToReleased).option.map(_.flatten) } @@ -821,12 +821,12 @@ final case class H2Storage(xa: Transactor[Task]) extends (StoreOp ~> Task) { WHERE lb.name = $name AND lb.major_version = ${mv.toString} ORDER BY r.release_id DESC LIMIT 1 - """.query[(String,String,Instant,Long,String)] + """.query[(String,String,Instant,String,String)] query.map(rowToReleased).option.map(_.flatten) } - type ReleaseTuple = (String,String,Instant,Option[Long],Option[String],Option[String],String,String,Long,Instant,Option[String],GUID) + type ReleaseTuple = (String,String,Instant,Option[String],Option[String],Option[String],String,String,Long,Instant,Option[String],GUID) // TIM: major, major hack. tpolecat could not suggest better though, // so for now, we go with this to avoid duplicating the query. Essentially diff --git a/core/src/test/scala/AuditSpec.scala b/core/src/test/scala/AuditSpec.scala index 8c08bec7..48adb25e 100644 --- a/core/src/test/scala/AuditSpec.scala +++ b/core/src/test/scala/AuditSpec.scala @@ -100,7 +100,7 @@ class AuditSpec extends NelsonSuite with BeforeAndAfterEach { val audit = new Auditor(config.auditQueue,defaultSystemLogin) val foo = Foo(1) - val releaseId = Option(10L) + val releaseId = Option("10") audit.write(foo, CreateAction, releaseId = releaseId).run audit.process(storage).take(1).run.run diff --git a/core/src/test/scala/Fixtures.scala b/core/src/test/scala/Fixtures.scala index 79785182..878961cf 100644 --- a/core/src/test/scala/Fixtures.scala +++ b/core/src/test/scala/Fixtures.scala @@ -98,7 +98,7 @@ object Fixtures { id <- choose(1000,10000) name <- alphaNumStr avatar <- arbitrary[URI] - } yield Organization(id.toLong,Option(name), name, avatar) + } yield Organization(id.toLong,Option(name), name, Option(avatar)) def genUser: Gen[User] = for { login <- alphaNumStr @@ -364,7 +364,7 @@ object Fixtures { a <- choose(1,10000) b <- genVersion } yield Github.Release( - id = a.toLong, + id = a.toString, url = "", htmlUrl = "", assets = Nil, diff --git a/core/src/test/scala/GithubFixtures.scala b/core/src/test/scala/GithubFixtures.scala index c69b4ca1..b4991fa6 100644 --- a/core/src/test/scala/GithubFixtures.scala +++ b/core/src/test/scala/GithubFixtures.scala @@ -40,7 +40,7 @@ object GitFixtures { OrgKey(1L, "slug 1") ) - val orgs = List(Organization(0L, Some("name"),"slug",new java.net.URI("avatar"))) + val orgs = List(Organization(0L, Some("name"),"slug",Option(new java.net.URI("avatar")))) val asset = Asset(0, "manifest.deployable.v1.b.yml", "", "", Some("content")) @@ -50,7 +50,7 @@ object GitFixtures { Some(Contents(encoded,"manifest.deployable.v1.b.yml",encoded.length.toLong)) } - def release(id: ID) = Release(id, + def release(id: String) = Release(id, "https://github.example.com/api/v3/repos/tim/howdy/releases/250", "https://github.example.com/tim/howdy/releases/tag/0.13.17", Seq(Asset(119, @@ -88,7 +88,7 @@ object GitFixtures { case GetUser(token: AccessToken) => token match { - case AccessToken("crash") => + case AccessToken("crash", _) => Task.fail(new Exception("Crash!")) case _ => loadResourceAsString("/nelson/github.user.json") @@ -106,7 +106,7 @@ object GitFixtures { case GetReleaseAssetContent(asset: Github.Asset, t: AccessToken) => Task.now(asset) - case GetRelease(slug: Slug, releaseId: ID, t: AccessToken) => + case GetRelease(slug: Slug, releaseId: String, t: AccessToken) => loadResourceAsString("/nelson/github.release.json") .flatMap(fromJson[Github.Release]) diff --git a/core/src/test/scala/GithubSpec.scala b/core/src/test/scala/GithubSpec.scala index ae8b4532..d118fa42 100644 --- a/core/src/test/scala/GithubSpec.scala +++ b/core/src/test/scala/GithubSpec.scala @@ -53,8 +53,8 @@ class GithubSpec extends FlatSpec with Matchers with BeforeAndAfterEach with Pro } it should "return release" in { - val r = release(250L) - val req = Github.Request.fetchRelease(slug, 0L)(token).runWith(interpreter).attemptRun + val r = release("250") + val req = Github.Request.fetchRelease(slug, "0")(token).runWith(interpreter).attemptRun req should equal(\/-(r)) } diff --git a/core/src/test/scala/ManifestSpec.scala b/core/src/test/scala/ManifestSpec.scala index c03d11a8..b2a3d6a3 100644 --- a/core/src/test/scala/ManifestSpec.scala +++ b/core/src/test/scala/ManifestSpec.scala @@ -115,7 +115,7 @@ class ManifestManualSpec extends NelsonSuite { content = Some(c) ) e = Github.Release( - id = 123, + id = "123", url = "", htmlUrl = "", assets = List(d), diff --git a/core/src/test/scala/NelsonSpec.scala b/core/src/test/scala/NelsonSpec.scala index aa93e525..c1c2ecb0 100644 --- a/core/src/test/scala/NelsonSpec.scala +++ b/core/src/test/scala/NelsonSpec.scala @@ -133,7 +133,7 @@ class NelsonSpec extends NelsonSuite with BeforeAndAfterEach { avatar = new java.net.URI("uri"), name = "user", email = Some("user@example.com"), - orgs = List(Organization(0L, Some("scalatest"), "slug", new java.net.URI("uri"))) + orgs = List(Organization(0L, Some("scalatest"), "slug", Option(new java.net.URI("uri")))) ) ) val md = Datacenter.ManualDeployment(testName, "dev", "manual-deployment", "1.1.1", "hash", "description", 9000) diff --git a/core/src/test/scala/ReleaseDBSpec.scala b/core/src/test/scala/ReleaseDBSpec.scala index 4edbbfad..369dafc8 100644 --- a/core/src/test/scala/ReleaseDBSpec.scala +++ b/core/src/test/scala/ReleaseDBSpec.scala @@ -59,6 +59,6 @@ class ReleaseDBSpec extends NelsonSuite with BeforeAndAfterEach { it should "find latest release for loadbalancer" in { val r = storage.run(config.storage, StoreOp.getLatestReleaseForLoadbalancer("lb", MajorVersion(1))).run r.map(_.version) should equal (Some(Version(1,2,3))) - r.map(_.releaseId) should equal (Some(123L)) + r.map(_.releaseId) should equal (Some("123")) } } diff --git a/core/src/test/scala/RepoDBSpec.scala b/core/src/test/scala/RepoDBSpec.scala index c7a199b8..b25628c7 100644 --- a/core/src/test/scala/RepoDBSpec.scala +++ b/core/src/test/scala/RepoDBSpec.scala @@ -27,7 +27,7 @@ class RepoDBSpec extends FlatSpec with Matchers with BeforeAndAfterEach { val storage = TestStorage.storage("RepoSpec") val uri = new java.net.URI("http://foo") - val user = User("login", uri, "user", None, List(Organization(0L, Some("org1"), "org1", uri),Organization(1L, Some("org2"), "org2", uri))) + val user = User("login", uri, "user", None, List(Organization(0L, Some("org1"), "org1", Option(uri)),Organization(1L, Some("org2"), "org2", Option(uri)))) val slug1 = Slug("org1","repo1") val repo1 = Repo(1, slug1.toString, RepoAccess.Admin.toString, Some(Hook(0L, true))).toOption.get diff --git a/core/src/test/scala/RoutingFixtures.scala b/core/src/test/scala/RoutingFixtures.scala index c5f86590..b00cbefb 100644 --- a/core/src/test/scala/RoutingFixtures.scala +++ b/core/src/test/scala/RoutingFixtures.scala @@ -156,24 +156,24 @@ import doobie.imports._ val slug = Slug("owner","RoutingTableSpec") val repo = Repo(9999L, slug.toString, RepoAccess.Admin.toString, None) - val release100 = Github.Release(100L, "", "", Nil, "1.0.0") - val release110 = Github.Release(110L, "", "", Nil, "1.1.0") - val release110100 = Github.Release(110100L, "", "", Nil, "1.10.100") - val release111 = Github.Release(111L, "", "", Nil, "1.1.1") - val release200 = Github.Release(200L, "", "", Nil, "2.0.0") - val release222 = Github.Release(222L, "", "", Nil, "2.2.2") - val release221 = Github.Release(221L, "", "", Nil, "2.2.1") - val release122 = Github.Release(122L, "", "", Nil, "1.2.2") - val release123 = Github.Release(123L, "", "", Nil, "1.2.3") - val release300 = Github.Release(300L, "", "", Nil, "3.0.0") - val release310 = Github.Release(310L, "", "", Nil, "3.1.0") - val release311 = Github.Release(311L, "", "", Nil, "3.1.1") - val release410 = Github.Release(410L, "", "", Nil, "4.1.0") - val release510 = Github.Release(510L, "", "", Nil, "5.1.0") - val release600 = Github.Release(600L, "", "", Nil, "6.0.0") - val release610 = Github.Release(610L, "", "", Nil, "6.1.0") - val release620 = Github.Release(620L, "", "", Nil, "6.2.0") - val release621 = Github.Release(621L, "", "", Nil, "6.2.1") + val release100 = Github.Release("100", "", "", Nil, "1.0.0") + val release110 = Github.Release("110", "", "", Nil, "1.1.0") + val release110100 = Github.Release("110100", "", "", Nil, "1.10.100") + val release111 = Github.Release("111", "", "", Nil, "1.1.1") + val release200 = Github.Release("200", "", "", Nil, "2.0.0") + val release222 = Github.Release("222", "", "", Nil, "2.2.2") + val release221 = Github.Release("221", "", "", Nil, "2.2.1") + val release122 = Github.Release("122", "", "", Nil, "1.2.2") + val release123 = Github.Release("123", "", "", Nil, "1.2.3") + val release300 = Github.Release("300", "", "", Nil, "3.0.0") + val release310 = Github.Release("310", "", "", Nil, "3.1.0") + val release311 = Github.Release("311", "", "", Nil, "3.1.1") + val release410 = Github.Release("410", "", "", Nil, "4.1.0") + val release510 = Github.Release("510", "", "", Nil, "5.1.0") + val release600 = Github.Release("600", "", "", Nil, "6.0.0") + val release610 = Github.Release("610", "", "", Nil, "6.1.0") + val release620 = Github.Release("620", "", "", Nil, "6.2.0") + val release621 = Github.Release("621", "", "", Nil, "6.2.1") val ingestReleases: StoreOpF[Unit] = StoreOp.createRelease(repo.toOption.get.id, release100) >> diff --git a/http/src/main/scala/plans/Auth.scala b/http/src/main/scala/plans/Auth.scala index bbde898e..6efea70a 100644 --- a/http/src/main/scala/plans/Auth.scala +++ b/http/src/main/scala/plans/Auth.scala @@ -57,7 +57,7 @@ final case class Auth(config: NelsonConfig) extends Default { case req @ POST -> Root / "auth" / "github" => decode[AccessToken](req){ tk => (for { - a <- Nelson.createSessionFromGithubToken(tk)(cfg).attemptRun + a <- Nelson.createSessionFromGithubToken(tk.copy(isPrivate = true))(cfg).attemptRun b <- cfg.security.authenticator.serialize(a) } yield (a.expiry, b)).fold( e => Task.fail(new RuntimeException(e.toString)), diff --git a/http/src/main/scala/plans/Default.scala b/http/src/main/scala/plans/Default.scala index 580e1522..9953126f 100644 --- a/http/src/main/scala/plans/Default.scala +++ b/http/src/main/scala/plans/Default.scala @@ -164,8 +164,12 @@ object ClientValidation { def filterUserAgent(service: HttpService) (config: NelsonConfig): HttpService = HttpService.lift { req => - val maybeUserAgent = req.headers.get(headers.`User-Agent`) - if (isAllowedUserAgent(maybeUserAgent)(config.bannedClients)) service(req) - else BadRequest("User-Agent not allowed. Please upgrade your client to the latest version.") + config.git match { + case _: ScmConfig.GitlabConfig => service(req) // bypasses this check because of http://bit.ly/2lgKCzW + case _ => + val maybeUserAgent = req.headers.get(headers.`User-Agent`) + if (isAllowedUserAgent(maybeUserAgent)(config.bannedClients)) service(req) + else BadRequest("User-Agent not allowed. Please upgrade your client to the latest version.") + } } } diff --git a/http/src/test/scala/DatacentersSpec.scala b/http/src/test/scala/DatacentersSpec.scala index 85bf69ac..63eb1fa7 100644 --- a/http/src/test/scala/DatacentersSpec.scala +++ b/http/src/test/scala/DatacentersSpec.scala @@ -53,7 +53,7 @@ class DatacentersSpec extends ServiceSpec { } it should "not allow any user to create manual deployments" in { - val config0 = config.copy(git = config.git.copy(organizationAdminList = Nil)) + val config0 = config.copy(config.git.withOrganizationAdminList(Nil)) val service = Datacenters(config0).service val req = Request(POST, uri("/v1/deployments")).authed .withBody(manual.asJson) diff --git a/http/src/test/scala/ServiceSpec.scala b/http/src/test/scala/ServiceSpec.scala index 54dd2d18..0fb4a123 100644 --- a/http/src/test/scala/ServiceSpec.scala +++ b/http/src/test/scala/ServiceSpec.scala @@ -27,7 +27,7 @@ trait ServiceSpec extends NelsonSuite { avatar = new java.net.URI("uri"), name = "user", email = Some("user@verizon.net"), - orgs = List(Organization(0L, Some("scalatest"), "slug", new java.net.URI("uri"))) + orgs = List(Organization(0L, Some("scalatest"), "slug", Option(new java.net.URI("uri")))) ) )