diff --git a/Manifest.toml b/Manifest.toml index ebed2653c7..ad121a1b07 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,5 +1,11 @@ # This file is machine-generated - editing it directly is not advised +[[Artifacts]] +deps = ["Pkg"] +git-tree-sha1 = "c30985d8821e0cd73870b17b0ed0ce6dc44cb744" +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.3.0" + [[AssetRegistry]] deps = ["Distributed", "JSON", "Pidfile", "SHA", "Test"] git-tree-sha1 = "b25e88db7944f98789130d7b503276bc34bc098e" @@ -32,6 +38,11 @@ uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" deps = ["Random", "Serialization", "Sockets"] uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" +[[ExprTools]] +git-tree-sha1 = "10407a39b87f29d47ebaca8edbc75d7c302ff93e" +uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" +version = "0.1.3" + [[FileWatching]] uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" @@ -74,6 +85,11 @@ git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" uuid = "82899510-4779-5014-852e-03e436cf321d" version = "1.0.0" +[[JLLWrappers]] +git-tree-sha1 = "c70593677bbf2c3ccab4f7500d0f4dacfff7b75c" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.1.3" + [[JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" @@ -131,6 +147,12 @@ version = "2.16.6+1" [[Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" +[[Mocking]] +deps = ["ExprTools"] +git-tree-sha1 = "916b850daad0d46b8c71f65f719c49957e9513ed" +uuid = "78c3b35d-d492-501b-9361-3d52fe80e533" +version = "0.7.1" + [[Mustache]] deps = ["Printf", "Tables"] git-tree-sha1 = "fcfc8266461f2905534aa00c0fc59b8751b1026e" @@ -228,7 +250,7 @@ uuid = "c2297ded-f4af-51ae-bb23-16f91089e4e1" version = "1.2.1" [[ZeroMQ_jll]] -deps = ["Libdl", "Pkg"] -git-tree-sha1 = "733352667c60ce39dfd3017db9b798b288c87417" +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "bba617292e040408cb72baa03c20f43583bf239f" uuid = "8f1865be-045e-5c20-9c9f-bfbfb0764568" -version = "4.3.2+4" +version = "4.3.2+5" diff --git a/Project.toml b/Project.toml index ff3a6b60bb..15c2a30707 100644 --- a/Project.toml +++ b/Project.toml @@ -16,6 +16,7 @@ JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d" +Mocking = "78c3b35d-d492-501b-9361-3d52fe80e533" Mustache = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70" Mux = "a975b10e-0019-58db-a62f-e48ff68538c9" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" diff --git a/src/webui/gitutils.jl b/src/webui/gitutils.jl index 885686ae75..1c8bb4e1e3 100644 --- a/src/webui/gitutils.jl +++ b/src/webui/gitutils.jl @@ -1,4 +1,5 @@ using ..Registrator: decodeb64 +using Mocking # # Run some GitForge function, warning on error but still returning the value. macro gf(ex::Expr) @@ -25,13 +26,23 @@ getrepo(::GitLabAPI, owner::AbstractString, name::AbstractString) = getrepo(::GitHubAPI, owner::AbstractString, name::AbstractString) = @gf get_repo(PROVIDERS["github"].client, owner, name) + +abstract type AuthResult end +struct AuthSuccess <: AuthResult end +struct AuthFailure <: AuthResult + reason::AbstractString +end + +is_success(res::AuthSuccess) = true +is_success(res::AuthFailure) = false + # Check for a user's authorization to release a package. # The criteria is simply whether the user is a collaborator for user-owned repos, # or whether they're an organization member or collaborator for organization-owned repos. -isauthorized(u, repo) = false +isauthorized(u, repo) = AuthFailure("Unkown user type or repo type") function isauthorized(u::User{GitHub.User}, repo::GitHub.Repo) if !get(CONFIG, "allow_private", false) - repo.private && return false + repo.private && return AuthFailure("Repo $(repo.name) is private") end if repo.private @@ -40,20 +51,29 @@ function isauthorized(u::User{GitHub.User}, repo::GitHub.Repo) forge = u.forge end - hasauth = if repo.organization === nothing - @gf is_collaborator(forge, repo.owner.login, repo.name, u.user.login) + if repo.organization === nothing + hasauth = @gf @mock is_collaborator(forge, repo.owner.login, repo.name, u.user.login) + if something(hasauth, false) + return AuthSuccess() + else + return AuthFailure("User $(u.user.login) is not a collaborator on repo $(repo.name)") + end else # First check for organization membership, and fall back to collaborator status. - ismember = @gf is_member(forge, repo.organization.login, u.user.login) - something(ismember, false) || - @gf is_collaborator(forge, repo.organization.login, repo.name, u.user.login) + ismember = @gf @mock is_member(forge, repo.organization.login, u.user.login) + hasauth = something(ismember, false) || + @gf @mock is_collaborator(forge, repo.organization.login, repo.name, u.user.login) + if something(hasauth, false) + return AuthSuccess() + else + return AuthFailure("User $(u.user.login) is not a member of the org $(repo.organization.login) and not a collaborator on repo $(repo.name)") + end end - return something(hasauth, false) end function isauthorized(u::User{GitLab.User}, repo::GitLab.Project) if !get(CONFIG, "allow_private", false) - repo.visibility == "private" && return false + repo.visibility == "private" && return AuthFailure("Project $(repo.name) is private") end if repo.visibility == "private" @@ -62,23 +82,31 @@ function isauthorized(u::User{GitLab.User}, repo::GitLab.Project) forge = u.forge end - hasauth = if repo.namespace.kind == "user" - @gf is_collaborator(forge, repo.owner.username, repo.name, u.user.id) + if repo.namespace.kind == "user" + hasauth = @gf @mock is_collaborator(forge, repo.owner.username, repo.name, u.user.id) + if something(hasauth, false) + return AuthSuccess() + else + return AuthFailure("User $(u.user.name) is not a member of project $(repo.name)") # GitLab terminology "member" (not "collaborator") + end else # Same as above: group membership then collaborator check. nspath = split(repo.namespace.full_path, "/") - ismember = @gf is_collaborator(u.forge, repo.namespace.full_path, repo.name, u.user.id) + ismember = @gf @mock is_collaborator(u.forge, repo.namespace.full_path, repo.name, u.user.id) if !something(ismember, false) accns = "" for ns in nspath accns = joinpath(accns, ns) - ismember = @gf is_member(forge, accns, u.user.id) + ismember = @gf @mock is_member(forge, accns, u.user.id) something(ismember, false) && break end end - ismember + if ismember + return AuthSuccess() + else + return AuthFailure("Project $(repo.name) belongs to the group $(repo.namespace.full_path), and user $(u.user.name) is not a member of that group or its parent group(s)") + end end - return something(hasauth, false) end # Get the raw (Julia)Project.toml text from a repository. diff --git a/src/webui/routes/register.jl b/src/webui/routes/register.jl index f5a752796f..3cabe71ebb 100644 --- a/src/webui/routes/register.jl +++ b/src/webui/routes/register.jl @@ -30,7 +30,10 @@ function register(r::HTTP.Request) owner, name = splitrepo(package) repo = getrepo(u.forge, owner, name) repo === nothing && return json(400; error="Repository was not found") - isauthorized(u, repo) || return json(400; error="Unauthorized to release this package") + auth_result = isauthorized(u, repo) + if !is_success(auth_result) + return json(400; error="Unauthorized to release this package. Reason: $(auth_result.reason)") + end # Get the (Julia)Project.toml, and make sure it is valid. toml = gettoml(u.forge, repo, ref) diff --git a/test/runtests.jl b/test/runtests.jl index 4b30f5eecd..6ff7784fc9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,5 +10,6 @@ if get(ENV, "TRAVIS", "") == "true" && !haskey(ENV, "GITHUB_API_TOKEN") else include("webui.jl") end +include("webui/gitutils.jl") end diff --git a/test/webui/gitutils.jl b/test/webui/gitutils.jl new file mode 100644 index 0000000000..c195278e36 --- /dev/null +++ b/test/webui/gitutils.jl @@ -0,0 +1,111 @@ +using Dates: DateTime +using Registrator.WebUI: isauthorized, AuthFailure, AuthSuccess, User +using GitForge: GitForge, GitHub, GitLab +using HTTP: stacktrace + +using Mocking + +Mocking.activate() + +function patch_gitforge(body::Function; is_collaborator=false, is_member=false) + patches = [ + @patch GitForge.is_collaborator(args...) = + GitForge.Result{Bool}(is_collaborator, nothing, nothing, stacktrace()) + @patch GitForge.is_member(args...) = + GitForge.Result{Bool}(is_member, nothing, nothing, stacktrace()) + ] + + apply(patches) do + return body() + end +end + +@testset "gitutils" begin + +@testset "isauthorized" begin + @test isauthorized("username", "reponame") == AuthFailure("Unkown user type or repo type") + + @testset "GitHub" begin + + user = GitHub.User(login="user123") + org = GitHub.User(login="JuliaLang") + private_repo = GitHub.Repo(name="Example.jl", private=true, owner=user) + public_repo_of_user = GitHub.Repo(name="Example.jl", private=false, owner=user, organization=nothing) + public_repo_of_org = GitHub.Repo(name="Example.jl", private=false, owner=org, organization=org) + u = User(user, GitHub.GitHubAPI()) + + @testset "private repo" begin + # Assuming CONFIG["allow_private"] is false + @test isauthorized(u, private_repo) == AuthFailure("Repo Example.jl is private") + end + + @testset "public repo of user" begin + # authorized if user is a collaborator on the repo + patch_gitforge(is_collaborator=true) do + @test isauthorized(u, public_repo_of_user) == AuthSuccess() + end + patch_gitforge(is_collaborator=false) do + @test isauthorized(u, public_repo_of_user) == AuthFailure("User user123 is not a collaborator on repo Example.jl") + end + end + + @testset "public repo of org" begin + # authorized if user is either a collaborator on the repo or member of the org + patch_gitforge(is_collaborator=true, is_member=true) do + @test isauthorized(u, public_repo_of_org) == AuthSuccess() + end + patch_gitforge(is_collaborator=true, is_member=false) do + @test isauthorized(u, public_repo_of_org) == AuthSuccess() + end + patch_gitforge(is_collaborator=false, is_member=true) do + @test isauthorized(u, public_repo_of_org) == AuthSuccess() + end + patch_gitforge(is_collaborator=false, is_member=false) do + @test isauthorized(u, public_repo_of_org) == AuthFailure("User user123 is not a member of the org JuliaLang and not a collaborator on repo Example.jl") + end + end + end + + @testset "GitLab" begin + + user = GitLab.User(name="user123", username="user123", id=111) + org = GitLab.User(name="org123", username="org123", id=222) + private_project = GitLab.Project(name="Example.jl", visibility="private", owner=user) + public_project_of_user = GitLab.Project(name="Example.jl", visibility="public", owner=user, namespace=GitLab.Namespace(kind="user")) + public_project_of_group = GitLab.Project(name="Example.jl", visibility="public", owner=org, namespace=GitLab.Namespace(kind="group", full_path="org123/subgroup/Example.jl")) + u = User(user, GitLab.GitLabAPI()) + + @testset "private project" begin + # Assuming CONFIG["allow_private"] is false + @test isauthorized(u, private_project) == AuthFailure("Project Example.jl is private") + end + + @testset "public project of user" begin + # authorized if user is a collaborator on the project + patch_gitforge(is_collaborator=true) do + @test isauthorized(u, public_project_of_user) == AuthSuccess() + end + patch_gitforge(is_collaborator=false) do + @test isauthorized(u, public_project_of_user) == AuthFailure("User user123 is not a member of project Example.jl") + end + end + + @testset "public project of group" begin + # authorized if user is a collaborator on the project or member of the group/subgroups + patch_gitforge(is_collaborator=true, is_member=true) do + @test isauthorized(u, public_project_of_group) == AuthSuccess() + end + patch_gitforge(is_collaborator=false, is_member=true) do + @test isauthorized(u, public_project_of_group) == AuthSuccess() + end + patch_gitforge(is_collaborator=true, is_member=false) do + @test isauthorized(u, public_project_of_group) == AuthSuccess() + end + patch_gitforge(is_collaborator=false, is_member=false) do + @test isauthorized(u, public_project_of_group) == AuthFailure("Project Example.jl belongs to the group org123/subgroup/Example.jl, and user user123 is not a member of that group or its parent group(s)") + end + end + end +end + +end \ No newline at end of file