harness-drone/handler/api/acl/check_test.go

787 lines
22 KiB
Go
Raw Normal View History

2019-02-19 23:56:41 +00:00
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
package acl
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/drone/drone/handler/api/errors"
"github.com/drone/drone/handler/api/request"
"github.com/drone/drone/core"
"github.com/google/go-cmp/cmp"
"github.com/go-chi/chi"
"github.com/golang/mock/gomock"
)
var noContext = context.Background()
// this test verifies that a 401 unauthorized error is written to
// the response if the client is not authenticated and repository
// visibility is internal or private.
func TestCheckAccess_Guest_Unauthorized(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithRepo(noContext, mockRepo),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckReadAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Must not invoke next handler in middleware chain")
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusUnauthorized; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
got, want := new(errors.Error), errors.ErrUnauthorized
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
// this test verifies the the next handler in the middleware
// chain is processed if the user is not authenticated BUT
// the repository is publicly visible.
func TestCheckAccess_Guest_PublicVisibility(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockRepo := *mockRepo
mockRepo.Visibility = core.VisibilityPublic
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithRepo(noContext, &mockRepo),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckReadAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusTeapot; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
}
// this test verifies that a 401 unauthorized error is written to
// the response if the repository visibility is internal, and the
// client is not authenticated.
func TestCheckAccess_Guest_InternalVisibility(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockRepo := *mockRepo
mockRepo.Visibility = core.VisibilityInternal
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithRepo(noContext, &mockRepo),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckReadAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Must not invoke next handler in middleware chain")
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusUnauthorized; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
}
// this test verifies the the next handler in the middleware
// chain is processed if the user is authenticated AND
// the repository is publicly visible.
func TestCheckAccess_Authenticated_PublicVisibility(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockRepo := *mockRepo
mockRepo.Visibility = core.VisibilityPublic
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithUser(
request.WithRepo(noContext, &mockRepo), mockUser),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckReadAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusTeapot; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
}
// this test verifies the the next handler in the middleware
// chain is processed if the user is authenticated AND
// the repository has internal visible.
func TestCheckAccess_Authenticated_InternalVisibility(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockRepo := *mockRepo
mockRepo.Visibility = core.VisibilityInternal
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithUser(
request.WithRepo(noContext, &mockRepo), mockUser),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckReadAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusTeapot; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
}
// this test verifies that a 404 not found error is written to
// the response if the repository is not found AND the user is
// authenticated.
func TestCheckAccess_Authenticated_RepositoryNotFound(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckReadAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Must not invoke next handler in middleware chain")
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusNotFound; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
got, want := new(errors.Error), errors.ErrNotFound
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
// this test verifies that a 404 not found error is written to
// the response if the user does not have permissions to access
// the repository.
func TestCheckAccess_Permission_NotFound(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithUser(
request.WithRepo(noContext, mockRepo), mockUser),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckReadAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Must not invoke next handler in middleware chain")
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusNotFound; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
got, want := new(errors.Error), errors.ErrNotFound
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
// this test verifies the the next handler in the middleware
// chain is processed if the user has read access to the
// repository.
func TestCheckReadAccess(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
readAccess := &core.Perm{
Synced: time.Now().Unix(),
Read: true,
Write: false,
Admin: false,
}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithUser(r.Context(), mockUser),
)
r = r.WithContext(
request.WithPerm(
request.WithUser(
request.WithRepo(noContext, mockRepo),
mockUser,
),
readAccess,
),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckReadAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusTeapot; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
}
// this test verifies that a 404 not found error is written to
// the response if the user lacks read access to the repository.
func TestCheckReadAccess_InsufficientPermissions(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
noAccess := &core.Perm{
Synced: time.Now().Unix(),
Read: false,
Write: false,
Admin: false,
}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithPerm(
request.WithUser(
request.WithRepo(noContext, mockRepo),
mockUser,
),
noAccess,
),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckReadAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Must not invoke next handler in middleware chain")
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusNotFound; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
got, want := new(errors.Error), errors.ErrNotFound
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
// this test verifies the the next handler in the middleware
// chain is processed if the user has write access to the
// repository.
func TestCheckWriteAccess(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
writeAccess := &core.Perm{
Synced: time.Now().Unix(),
Read: true,
Write: true,
Admin: false,
}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithPerm(
request.WithUser(
request.WithRepo(noContext, mockRepo),
mockUser,
),
writeAccess,
),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckWriteAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusTeapot; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
}
// this test verifies that a 404 not found error is written to
// the response if the user lacks write access to the repository.
//
// TODO(bradrydzewski) we should consider returning a 403 forbidden
// if the user has read access.
func TestCheckWriteAccess_InsufficientPermissions(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
noAccess := &core.Perm{
Synced: time.Now().Unix(),
Read: true,
Write: false,
Admin: false,
}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithPerm(
request.WithUser(
request.WithRepo(noContext, mockRepo),
mockUser,
),
noAccess,
),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckWriteAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Must not invoke next handler in middleware chain")
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusNotFound; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
got, want := new(errors.Error), errors.ErrNotFound
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
// this test verifies the the next handler in the middleware
// chain is processed if the user has admin access to the
// repository.
func TestCheckAdminAccess(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
noAccess := &core.Perm{
Synced: time.Now().Unix(),
Read: true,
Write: true,
Admin: true,
}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithPerm(
request.WithUser(
request.WithRepo(noContext, mockRepo),
mockUser,
),
noAccess,
),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckAdminAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusTeapot; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
}
// this test verifies that a 404 not found error is written to
// the response if the user lacks admin access to the repository.
//
// TODO(bradrydzewski) we should consider returning a 403 forbidden
// if the user has read access.
func TestCheckAdminAccess_InsufficientPermissions(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
noAccess := &core.Perm{
Synced: time.Now().Unix(),
Read: true,
Write: true,
Admin: false,
}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithPerm(
request.WithUser(
request.WithRepo(noContext, mockRepo),
mockUser,
),
noAccess,
),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckAdminAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Must not invoke next handler in middleware chain")
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusNotFound; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
got, want := new(errors.Error), errors.ErrNotFound
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
// this test verifies the the next handler in the middleware
// chain is processed if the authenticated user is a system
// administrator.
func TestCheckAdminAccess_SystemAdmin(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
user := &core.User{ID: 1, Admin: true}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithUser(r.Context(), user),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckAdminAccess())
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusTeapot; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
}
// this test verifies that a 401 unauthorized error is written to
// the response if the client is not authenticated and write
// access is required.
func TestCheckAccess_Guest_Write(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithRepo(noContext, mockRepo),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckAccess(true, true, false))
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Must not invoke next handler in middleware chain")
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusUnauthorized; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
got, want := new(errors.Error), errors.ErrUnauthorized
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
// this test verifies that a 401 unauthorized error is written to
// the response if the client is not authenticated and admin
// access is required.
func TestCheckAccess_Guest_Admin(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
r = r.WithContext(
request.WithRepo(noContext, mockRepo),
)
router := chi.NewRouter()
router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
router.Use(CheckAccess(true, false, true))
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Must not invoke next handler in middleware chain")
})
})
router.ServeHTTP(w, r)
if got, want := w.Code, http.StatusUnauthorized; got != want {
t.Errorf("Want status code %d, got %d", want, got)
}
got, want := new(errors.Error), errors.ErrUnauthorized
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
// // this test verifies the the next handler in the middleware
// // chain is processed if the authenticated has read permissions
// // that are successfully synchronized with the source.
// func TestCheckAccess_RefreshPerms(t *testing.T) {
// controller := gomock.NewController(t)
// defer controller.Finish()
// expiredAccess := &core.Perm{
// Synced: 0,
// Read: false,
// Write: false,
// Admin: false,
// }
// updatedAccess := &core.Perm{
// Read: true,
// Write: true,
// Admin: true,
// }
// checkPermUpdate := func(ctx context.Context, perm *core.Perm) {
// if perm.Synced == 0 {
// t.Errorf("Expect synced timestamp updated")
// }
// if perm.Read == false {
// t.Errorf("Expect Read flag updated")
// }
// if perm.Write == false {
// t.Errorf("Expect Write flag updated")
// }
// if perm.Admin == false {
// t.Errorf("Expect Admin flag updated")
// }
// }
// repos := mock.NewMockRepositoryStore(controller)
// repos.EXPECT().FindName(gomock.Any(), "octocat", "hello-world").Return(mockRepo, nil)
// perms := mock.NewMockPermStore(controller)
// perms.EXPECT().Find(gomock.Any(), mockRepo.UID, mockUser.ID).Return(expiredAccess, nil)
// perms.EXPECT().Update(gomock.Any(), expiredAccess).Return(nil).Do(checkPermUpdate)
// service := mock.NewMockRepositoryService(controller)
// service.EXPECT().FindPerm(gomock.Any(), "octocat/hello-world").Return(updatedAccess, nil)
// factory := mock.NewMockRepositoryServiceFactory(controller)
// factory.EXPECT().Create(mockUser).Return(service)
// w := httptest.NewRecorder()
// r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
// r = r.WithContext(
// request.WithUser(r.Context(), mockUser),
// )
// router := chi.NewRouter()
// router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
// router.Use(CheckReadAccess(factory, repos, perms))
// router.Get("/", func(w http.ResponseWriter, r *http.Request) {
// w.WriteHeader(http.StatusTeapot)
// })
// })
// router.ServeHTTP(w, r)
// if got, want := w.Code, http.StatusTeapot; got != want {
// t.Errorf("Want status code %d, got %d", want, got)
// }
// }
// // this test verifies that a 404 not found error is written to
// // the response if the user permissions are expired and the
// // updated permissions cannot be fetched.
// func TestCheckAccess_RefreshPerms_Error(t *testing.T) {
// controller := gomock.NewController(t)
// defer controller.Finish()
// expiredAccess := &core.Perm{
// Synced: 0,
// Read: false,
// Write: false,
// Admin: false,
// }
// repos := mock.NewMockRepositoryStore(controller)
// repos.EXPECT().FindName(gomock.Any(), "octocat", "hello-world").Return(mockRepo, nil)
// perms := mock.NewMockPermStore(controller)
// perms.EXPECT().Find(gomock.Any(), mockRepo.UID, mockUser.ID).Return(expiredAccess, nil)
// service := mock.NewMockRepositoryService(controller)
// service.EXPECT().FindPerm(gomock.Any(), "octocat/hello-world").Return(nil, io.EOF)
// factory := mock.NewMockRepositoryServiceFactory(controller)
// factory.EXPECT().Create(mockUser).Return(service)
// w := httptest.NewRecorder()
// r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
// r = r.WithContext(
// request.WithUser(r.Context(), mockUser),
// )
// router := chi.NewRouter()
// router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
// router.Use(CheckReadAccess(factory, repos, perms))
// router.Get("/", func(w http.ResponseWriter, r *http.Request) {
// w.WriteHeader(http.StatusTeapot)
// })
// })
// router.ServeHTTP(w, r)
// if got, want := w.Code, 404; got != want {
// t.Errorf("Want status code %d, got %d", want, got)
// }
// }
// // this test verifies the the next handler in the middleware
// // chain is processed if the user permissions are expired,
// // updated permissions are fetched, but fail the changes fail
// // to persist to the database. We know the user has access,
// // so we allow them to proceed even in the event of a failure.
// func TestCheckAccess_RefreshPerms_CannotSave(t *testing.T) {
// controller := gomock.NewController(t)
// defer controller.Finish()
// expiredAccess := &core.Perm{
// Synced: 0,
// Read: false,
// Write: false,
// Admin: false,
// }
// updatedAccess := &core.Perm{
// Read: true,
// Write: true,
// Admin: true,
// }
// service := mock.NewMockRepositoryService(controller)
// service.EXPECT().FindPerm(gomock.Any(), "octocat/hello-world").Return(updatedAccess, nil)
// factory := mock.NewMockRepositoryServiceFactory(controller)
// factory.EXPECT().Create(mockUser).Return(service)
// repos := mock.NewMockRepositoryStore(controller)
// repos.EXPECT().FindName(gomock.Any(), "octocat", "hello-world").Return(mockRepo, nil)
// perms := mock.NewMockPermStore(controller)
// perms.EXPECT().Find(gomock.Any(), mockRepo.UID, mockUser.ID).Return(expiredAccess, nil)
// perms.EXPECT().Update(gomock.Any(), expiredAccess).Return(io.EOF)
// w := httptest.NewRecorder()
// r := httptest.NewRequest("GET", "/api/repos/octocat/hello-world", nil)
// r = r.WithContext(
// request.WithUser(r.Context(), mockUser),
// )
// router := chi.NewRouter()
// router.Route("/api/repos/{owner}/{name}", func(router chi.Router) {
// router.Use(CheckReadAccess(factory, repos, perms))
// router.Get("/", func(w http.ResponseWriter, r *http.Request) {
// w.WriteHeader(http.StatusTeapot)
// })
// })
// router.ServeHTTP(w, r)
// if got, want := w.Code, http.StatusTeapot; got != want {
// t.Errorf("Want status code %d, got %d", want, got)
// }
// }