Skip to content

Commit

Permalink
Merge pull request #3 from jasoet/adminAuth
Browse files Browse the repository at this point in the history
[Jasoet] Enable Admin Authorization
  • Loading branch information
walbertus authored Sep 20, 2019
2 parents 70fef21 + 7a9027a commit 34a5cdf
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 40 deletions.
2 changes: 0 additions & 2 deletions internal/app/cli/daemon/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,6 @@ func (s *ClientTestSuite) TestRemoveScheduledJobWithInvalidJobID() {
httpmock.Activate()
defer httpmock.DeactivateAndReset()


mockResponse := httpmock.NewStringResponse(400, "Invalid Job ID")
mockError := error(nil)
mockRequest(proctorConfig, "DELETE", fmt.Sprintf("http://"+proctorConfig.Host+ScheduleRoute+"/%s", jobID), mockResponse, mockError)
Expand All @@ -842,7 +841,6 @@ func (s *ClientTestSuite) TestRemoveScheduledJobWhenJobIDNotFound() {
httpmock.Activate()
defer httpmock.DeactivateAndReset()


mockResponse := httpmock.NewStringResponse(404, "Job not found")
mockError := error(nil)
mockRequest(proctorConfig, "DELETE", fmt.Sprintf("http://"+proctorConfig.Host+ScheduleRoute+"/%s", jobID), mockResponse, mockError)
Expand Down
22 changes: 11 additions & 11 deletions internal/app/service/execution/status/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ package status
type ExecutionStatus string

const (
Received ExecutionStatus = "RECEIVED"
RequirementNotMet ExecutionStatus = "REQUIREMENT_NOT_MET"
Created ExecutionStatus = "CREATED"
CreationFailed ExecutionStatus = "CREATION_FAILED"
JobCreationFailed ExecutionStatus = "JOB_CREATION_FAILED"
JobReady ExecutionStatus = "JOB_READY"
PodCreationFailed ExecutionStatus = "POD_CREATION_FAILED"
PodReady ExecutionStatus = "POD_READY"
PodFailed ExecutionStatus = "POD_FAILED"
FetchPodLogFailed ExecutionStatus = "FETCH_POD_LOG_FAILED"
Finished ExecutionStatus = "FINISHED"
Received ExecutionStatus = "RECEIVED"
RequirementNotMet ExecutionStatus = "REQUIREMENT_NOT_MET"
Created ExecutionStatus = "CREATED"
CreationFailed ExecutionStatus = "CREATION_FAILED"
JobCreationFailed ExecutionStatus = "JOB_CREATION_FAILED"
JobReady ExecutionStatus = "JOB_READY"
PodCreationFailed ExecutionStatus = "POD_CREATION_FAILED"
PodReady ExecutionStatus = "POD_READY"
PodFailed ExecutionStatus = "POD_FAILED"
FetchPodLogFailed ExecutionStatus = "FETCH_POD_LOG_FAILED"
Finished ExecutionStatus = "FINISHED"
)
19 changes: 13 additions & 6 deletions internal/app/service/infra/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ func GetStringDefault(viper *viper.Viper, key string, defaultValue string) strin
return viper.GetString(key)
}

func GetArrayString(viper *viper.Viper, key string) []string {
return strings.Split(viper.GetString(key), ",")
}

func GetArrayStringDefault(viper *viper.Viper, key string, defaultValue []string) []string {
viper.SetDefault(key, strings.Join(defaultValue, ","))
return strings.Split(viper.GetString(key), ",")
}

func GetBoolDefault(viper *viper.Viper, key string, defaultValue bool) bool {
viper.SetDefault(key, defaultValue)
return viper.GetBool(key)
Expand Down Expand Up @@ -85,6 +94,7 @@ type ProctorConfig struct {
AuthEnabled bool
NotificationPluginBinary []string
NotificationPluginExported []string
AuthRequiredAdminGroup []string
}

func Load() ProctorConfig {
Expand Down Expand Up @@ -129,14 +139,11 @@ func Load() ProctorConfig {
AuthPluginBinary: fang.GetString("AUTH_PLUGIN_BINARY"),
AuthPluginExported: GetStringDefault(fang, "AUTH_PLUGIN_EXPORTED", "Auth"),
AuthEnabled: GetBoolDefault(fang, "AUTH_ENABLED", false),
NotificationPluginBinary: GetArrayString(fang, "NOTIFICATION_PLUGIN_BINARY"),
NotificationPluginExported: GetArrayString(fang, "NOTIFICATION_PLUGIN_EXPORTED"),
AuthRequiredAdminGroup: GetArrayStringDefault(fang, "AUTH_REQUIRED_ADMIN_GROUP", []string{"proctor_admin"}),
}

notificationPluginsBinary := strings.Split(fang.GetString("NOTIFICATION_PLUGIN_BINARY"), ",")
proctorConfig.NotificationPluginBinary = append(proctorConfig.NotificationPluginBinary, notificationPluginsBinary...)

notificationPluginsExported := strings.Split(fang.GetString("NOTIFICATION_PLUGIN_EXPORTED"), ",")
proctorConfig.NotificationPluginExported = append([]string{}, notificationPluginsExported...)

return proctorConfig
}

Expand Down
23 changes: 11 additions & 12 deletions internal/app/service/infra/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,20 @@ type GoPlugin interface {
type goPlugin struct{}

func (g *goPlugin) Load(pluginBinary string, exportedName string) (plugin.Symbol, error) {
binary, err := plugin.Open(pluginBinary)
logger.LogErrors(err, "load auth plugin binary from location: ", pluginBinary)
if err != nil {
return nil, fmt.Errorf("failed to load plugin binary from location: %s", pluginBinary)
}
binary, err := plugin.Open(pluginBinary)
logger.LogErrors(err, "load auth plugin binary from location: ", pluginBinary)
if err != nil {
return nil, fmt.Errorf("failed to load plugin binary from location: %s", pluginBinary)
}

raw, err := binary.Lookup(exportedName)
logger.LogErrors(err, "Lookup ", pluginBinary, " for ", exportedName)
if err != nil {
return nil, fmt.Errorf("failed to Lookup plugin binary from location: %s with Exported Name: %s", pluginBinary, exportedName)
}
return raw, nil
raw, err := binary.Lookup(exportedName)
logger.LogErrors(err, "Lookup ", pluginBinary, " for ", exportedName)
if err != nil {
return nil, fmt.Errorf("failed to Lookup plugin binary from location: %s with Exported Name: %s", pluginBinary, exportedName)
}
return raw, nil
}

func NewGoPlugin() GoPlugin {
return &goPlugin{}
}

54 changes: 54 additions & 0 deletions internal/app/service/security/middleware/admin_authorization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package middleware

import (
"net/http"
"proctor/internal/app/service/security/service"

"github.com/gorilla/mux"

"proctor/internal/app/service/infra/config"
"proctor/internal/app/service/infra/logger"
"proctor/pkg/auth"
)

type adminAuthorizationMiddleware struct {
service service.SecurityService
requiredGroup []string
enabled bool
}

func (middleware *adminAuthorizationMiddleware) Secure(router *mux.Router, path string, handler http.Handler) *mux.Route {
return router.NewRoute().Path(path).Handler(middleware.MiddlewareFunc(handler))
}

func (middleware *adminAuthorizationMiddleware) MiddlewareFunc(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !middleware.enabled {
next.ServeHTTP(w, r)
return
}

userDetail := r.Context().Value(ContextUserDetailKey)
if userDetail == nil {
w.WriteHeader(http.StatusUnauthorized)
return
}

authorized, err := middleware.service.Verify(*(userDetail.(*auth.UserDetail)), middleware.requiredGroup)
logger.LogErrors(err, "authorization", middleware.requiredGroup, userDetail)
if !authorized {
w.WriteHeader(http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}

func NewAdminAuthorizationMiddleware(securityService service.SecurityService) AuthorizationMiddleware {
proctorConfig := config.Config()
return &adminAuthorizationMiddleware{
service: securityService,
requiredGroup: proctorConfig.AuthRequiredAdminGroup,
enabled: proctorConfig.AuthEnabled,
}
}
215 changes: 215 additions & 0 deletions internal/app/service/security/middleware/admin_authorization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package middleware

import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"

"proctor/internal/app/service/security/service"
"proctor/pkg/auth"
)

type adminAuthorizationContext struct {
authorizationMiddleware adminAuthorizationMiddleware
requestHandler func(http.Handler) http.Handler
securityService *service.SecurityServiceMock
}

func (context *adminAuthorizationContext) setUp(t *testing.T) {
context.authorizationMiddleware = adminAuthorizationMiddleware{}
context.requestHandler = func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
})
}
context.securityService = &service.SecurityServiceMock{}
context.authorizationMiddleware.service = context.securityService
context.authorizationMiddleware.enabled = true
context.authorizationMiddleware.requiredGroup = []string{"proctor_admin", "system"}
}

func (context *adminAuthorizationContext) tearDown() {
}

func (context *adminAuthorizationContext) instance() *adminAuthorizationContext {
return context
}

func newAdminAuthorizationContext() *adminAuthorizationContext {
return &adminAuthorizationContext{}
}

func TestAdminAuthorizationMiddleware_MiddlewareFuncExecutionSuccess(t *testing.T) {
ctx := newAdminAuthorizationContext()
ctx.setUp(t)
defer ctx.tearDown()

requestBody := map[string]string{}
requestBody["name"] = "a-job"
body, _ := json.Marshal(requestBody)
userDetail := &auth.UserDetail{
Name: "William Dembo",
Email: "[email protected]",
Active: true,
Groups: []string{"system", "proctor_admin"},
}

response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
requestContext := context.WithValue(request.Context(), ContextUserDetailKey, userDetail)
request = request.WithContext(requestContext)
requestHandler := ctx.instance().requestHandler

securityService := ctx.securityService
securityService.On("Verify", *userDetail, ctx.authorizationMiddleware.requiredGroup).Return(true, nil)

testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

authzMiddleware := ctx.instance().authorizationMiddleware
requestHandler(authzMiddleware.MiddlewareFunc(testHandler)).ServeHTTP(response, request)

responseResult := response.Result()
assert.Equal(t, http.StatusOK, responseResult.StatusCode)
}

func TestAdminAuthorizationMiddleware_MiddlewareFuncScheduleSuccess(t *testing.T) {
ctx := newAdminAuthorizationContext()
ctx.setUp(t)
defer ctx.tearDown()

requestBody := map[string]string{}
requestBody["jobName"] = "a-job"
body, _ := json.Marshal(requestBody)
userDetail := &auth.UserDetail{
Name: "William Dembo",
Email: "[email protected]",
Active: true,
Groups: []string{"system", "proctor_maintainer"},
}

response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
requestContext := context.WithValue(request.Context(), ContextUserDetailKey, userDetail)
request = request.WithContext(requestContext)
requestHandler := ctx.instance().requestHandler


securityService := ctx.securityService
securityService.On("Verify", *userDetail, ctx.authorizationMiddleware.requiredGroup).Return(true, nil)

testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

authzMiddleware := ctx.instance().authorizationMiddleware
requestHandler(authzMiddleware.MiddlewareFunc(testHandler)).ServeHTTP(response, request)

responseResult := response.Result()
assert.Equal(t, http.StatusOK, responseResult.StatusCode)
}

func TestAdminAuthorizationMiddleware_MiddlewareFuncWithoutUserDetail(t *testing.T) {
ctx := newAdminAuthorizationContext()
ctx.setUp(t)
defer ctx.tearDown()

requestBody := map[string]string{}
requestBody["name"] = "a-job"
body, _ := json.Marshal(requestBody)

response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
requestHandler := ctx.instance().requestHandler

testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

authzMiddleware := ctx.instance().authorizationMiddleware
requestHandler(authzMiddleware.MiddlewareFunc(testHandler)).ServeHTTP(response, request)

responseResult := response.Result()
assert.Equal(t, http.StatusUnauthorized, responseResult.StatusCode)
}

func TestAdminAuthorizationMiddleware_MiddlewareFuncFailed(t *testing.T) {
ctx := newAdminAuthorizationContext()
ctx.setUp(t)
defer ctx.tearDown()

requestBody := map[string]string{}
requestBody["name"] = "a-job"
body, _ := json.Marshal(requestBody)
userDetail := &auth.UserDetail{
Name: "William Dembo",
Email: "[email protected]",
Active: true,
Groups: []string{"system", "not_proctor_maintainer"},
}

response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
requestContext := context.WithValue(request.Context(), ContextUserDetailKey, userDetail)
request = request.WithContext(requestContext)
requestHandler := ctx.instance().requestHandler

securityService := ctx.securityService
securityService.On("Verify", *userDetail, ctx.authorizationMiddleware.requiredGroup).Return(false, nil)

testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

authzMiddleware := ctx.instance().authorizationMiddleware
requestHandler(authzMiddleware.MiddlewareFunc(testHandler)).ServeHTTP(response, request)

responseResult := response.Result()
assert.Equal(t, http.StatusForbidden, responseResult.StatusCode)
}

func TestAdminAuthorizationMiddleware_MiddlewareFuncDisabled(t *testing.T) {
ctx := newAdminAuthorizationContext()
ctx.setUp(t)
defer ctx.tearDown()

authzMiddleware := ctx.instance().authorizationMiddleware
authzMiddleware.enabled = false
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
ts := httptest.NewServer(authzMiddleware.MiddlewareFunc(testHandler))
defer ts.Close()

client := &http.Client{}
req, _ := http.NewRequest("GET", ts.URL, nil)

resp, _ := client.Do(req)
defer resp.Body.Close()

assert.Equal(t, http.StatusOK, resp.StatusCode)
}

func TestAdminAuthorizationMiddleware_Secure(t *testing.T) {
ctx := newAdminAuthorizationContext()
ctx.setUp(t)
defer ctx.tearDown()

router := mux.NewRouter()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
authzMiddleware := ctx.instance().authorizationMiddleware
securedRouter := authzMiddleware.Secure(router, "/secure/path", handler)

handledPath, err := securedRouter.GetPathTemplate()
assert.NoError(t, err)
assert.Equal(t, "/secure/path", handledPath)
}
2 changes: 1 addition & 1 deletion internal/app/service/security/middleware/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (middleware *authenticationMiddleware) isRequestExcluded(r *http.Request) b
}

func NewAuthenticationMiddleware(securityService service.SecurityService) Middleware {
proctorConfig := config.Load()
proctorConfig := config.Config()
return &authenticationMiddleware{
service: securityService,
enabled: proctorConfig.AuthEnabled,
Expand Down
Loading

0 comments on commit 34a5cdf

Please sign in to comment.