diff --git a/.gitignore b/.gitignore index 881fb9e8192042a5289a784d09fcac4421242eb4..a2cb4e04b9b8b2dc71fa54aba1a5cc820f51c1e8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ bin/ release/ tests/ +cover.html +cover.out *.iml config.yml diff --git a/Makefile b/Makefile index d86dd2ebe7dfdcef8e79c6e4ad3b495d7d0829b5..f11cee3764828e943d5451690f6fe9c75a595ad2 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,11 @@ format: @echo "--> Running go fmt" @go fmt $(PACKAGES) +coverage: + @echo "--> Running go coverage" + @go test -coverprofile cover.out + @go tool cover -html=cover.out -o cover.html + cover: @echo "--> Running go cover" @go test --cover diff --git a/build.go b/build.go index 92e253dcfae016cbe836c95e2fb0d5576d18634b..4f83b87c77e6d7690d053af96a6132e8a46c46ce 100644 --- a/build.go +++ b/build.go @@ -1,3 +1,3 @@ package main -const buildID = "v0.0.5, git+sha: 4c3ee98" +const buildID = "v0.0.5, git+sha: 81b7def" diff --git a/config_test.go b/config_test.go index 3f5bbcfda60c30a156622b71b3bb02d2c7a6c5ca..ec787823150407c488845806dfc7d7d48586b2a2 100644 --- a/config_test.go +++ b/config_test.go @@ -21,6 +21,12 @@ import ( "testing" ) +func TestNewDefaultConfig(t *testing.T) { + if config := newDefaultConfig(); config == nil { + t.Errorf("we should have recieved a config") + } +} + func TestReadConfiguration(t *testing.T) { testCases := []struct { Content string @@ -60,6 +66,97 @@ redirection_url: http://127.0.0.1:3000 } } +func TestIsConfig(t *testing.T) { + tests := []struct { + Config *Config + Ok bool + }{ + { + Config: &Config{}, + }, + { + Config: &Config{ + DiscoveryURL: "http://127.0.0.1:8080", + }, + }, + { + Config: &Config{ + DiscoveryURL: "http://127.0.0.1:8080", + ClientID: "client", + Secret: "client", + }, + }, + { + Config: &Config{ + Listen: ":8080", + DiscoveryURL: "http://127.0.0.1:8080", + ClientID: "client", + Secret: "client", + RedirectionURL: "http://120.0.0.1", + }, + }, + { + Config: &Config{ + Listen: ":8080", + DiscoveryURL: "http://127.0.0.1:8080", + ClientID: "client", + Secret: "client", + RedirectionURL: "http://120.0.0.1", + Upstream: "http://120.0.0.1", + }, + Ok: true, + }, + { + Config: &Config{ + Listen: ":8080", + SkipTokenVerification: true, + Upstream: "http://120.0.0.1", + }, + Ok: true, + }, + { + Config: &Config{ + DiscoveryURL: "http://127.0.0.1:8080", + ClientID: "client", + Secret: "client", + RedirectionURL: "http://120.0.0.1", + Upstream: "http://120.0.0.1", + }, + }, + { + Config: &Config{ + Listen: ":8080", + DiscoveryURL: "http://127.0.0.1:8080", + ClientID: "client", + Secret: "client", + RedirectionURL: "http://120.0.0.1", + }, + }, + { + Config: &Config{ + Listen: ":8080", + DiscoveryURL: "http://127.0.0.1:8080", + ClientID: "client", + Secret: "client", + RedirectionURL: "http://120.0.0.1", + Upstream: "this should fail", + }, + }, + } + + for i, c := range tests { + if err := c.Config.isValid(); err != nil && c.Ok { + t.Errorf("test case %d, the config should not have errored, error: %s", i, err) + } + } +} + +func TestGetOptions(t *testing.T) { + if flags := getOptions(); flags == nil { + t.Errorf("we should have received some flags options") + } +} + func writeFakeConfigFile(t *testing.T, content string) *os.File { f, err := ioutil.TempFile("", "node_label_file") if err != nil { diff --git a/cover_test.go b/cover_test.go index 4ca5a5ce1585fed56ca2f88f50177bd143b36258..d087cb87a8f5c1cfc99ffae674f7098462e6438b 100644 --- a/cover_test.go +++ b/cover_test.go @@ -1 +1,16 @@ -package keycloak_proxy +/* +Copyright 2015 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 main diff --git a/doc.go b/doc.go index 50bd787249bef30fbb4ddc94fa05fd2e3260a1f8..e2878d8b92b8bd373200fb899f4f4c8e75449dc5 100644 --- a/doc.go +++ b/doc.go @@ -27,7 +27,7 @@ import ( const ( prog = "keycloak-proxy" - version = "v0.0.5" + version = "v0.0.6" author = "Rohith" email = "gambol99@gmail.com" description = "is a proxy using the keycloak service for auth and authorization" @@ -72,8 +72,6 @@ type Resource struct { // Config is the configuration for the proxy type Config struct { - // Verbose switches on debug logging - Verbose bool `json:"verbose" yaml:"verbose"` // LogRequests indicates if we should log all the requests LogRequests bool `json:"log_requests" yaml:"log_requests"` // LogFormat is the logging format @@ -116,6 +114,10 @@ type Config struct { ForbiddenPage string `json:"forbidden_page" yaml:"forbidden_page"` // SkipTokenVerification tells the service to skipp verifying the access token - for testing purposes SkipTokenVerification bool + // Verbose switches on debug logging + Verbose bool `json:"verbose" yaml:"verbose"` + // Hostname is a list of hostnames the service should response to + Hostnames []string `json:"hostnames" yaml:"hostname"` } // KeycloakProxy is the server component diff --git a/handlers_test.go b/handlers_test.go index 4ca5a5ce1585fed56ca2f88f50177bd143b36258..b619853339359759cdeb3b855c891162be742cc2 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -1 +1,149 @@ -package keycloak_proxy +/* +Copyright 2015 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 main + +import ( + "net/http" + "testing" + + "github.com/gambol99/go-oidc/jose" + "github.com/gin-gonic/gin" +) + +func TestEntrypointHandler(t *testing.T) { + proxy := newFakeKeycloakProxy(t) + handler := proxy.entrypointHandler() + + tests := []struct { + Context *gin.Context + Secure bool + }{ + { + Context: newFakeGinContext("GET", fakeAdminRoleURL), Secure: true, + }, + { + Context: newFakeGinContext("GET", fakeAdminRoleURL+"/sso"), Secure: true, + }, + { + Context: newFakeGinContext("GET", fakeAdminRoleURL+"/../sso"), Secure: true, + }, + { + Context: newFakeGinContext("GET", "/not_secure"), + }, + { + Context: newFakeGinContext("GET", oauthURL), + }, + } + + for i, c := range tests { + handler(c.Context) + if _, found := c.Context.Get(cxEnforce); c.Secure && !found { + t.Errorf("test case %d should have been set secure", i) + } + } +} + +func TestAdmissionHandler(t *testing.T) { + proxy := newFakeKeycloakProxy(t) + handler := proxy.admissionHandler() + tests := []struct { + Context *gin.Context + Resource *Resource + UserContext *userContext + HTTPCode int + }{ + { + Context: newFakeGinContext("GET", ""), + HTTPCode: http.StatusOK, + }, + { + Context: newFakeGinContext("GET", "/admin"), + HTTPCode: http.StatusForbidden, + Resource: &Resource{ + URL: fakeAdminRoleURL, + Methods: []string{"GET"}, + RolesAllowed: []string{fakeAdminRole}, + }, + UserContext: &userContext{ + roles: []string{}, + }, + }, + { + Context: newFakeGinContext("GET", fakeAdminRoleURL), + HTTPCode: http.StatusOK, + Resource: &Resource{ + URL: fakeAdminRoleURL, + Methods: []string{"GET"}, + RolesAllowed: []string{fakeAdminRole}, + }, + UserContext: &userContext{ + roles: []string{fakeAdminRole}, + claims: jose.Claims{"aud": fakeClientID}, + }, + }, + { + Context: newFakeGinContext("GET", fakeAdminRoleURL+"/sso"), + HTTPCode: http.StatusOK, + Resource: &Resource{ + URL: fakeAdminRoleURL, + Methods: []string{"GET"}, + RolesAllowed: []string{fakeAdminRole}, + }, + UserContext: &userContext{ + roles: []string{fakeTestRole, fakeAdminRole}, + claims: jose.Claims{"aud": fakeClientID}, + }, + }, + { + Context: newFakeGinContext("GET", fakeTestRoleURL), + HTTPCode: http.StatusForbidden, + Resource: &Resource{ + URL: fakeAdminRoleURL, + Methods: []string{"GET"}, + RolesAllowed: []string{fakeTestRole, "test"}, + }, + UserContext: &userContext{ + roles: []string{fakeTestRole, fakeAdminRole}, + claims: jose.Claims{"aud": fakeClientID}, + }, + }, + { + Context: newFakeGinContext("GET", fakeAdminRoleURL), + HTTPCode: http.StatusForbidden, + Resource: &Resource{ + URL: fakeAdminRoleURL, + Methods: []string{"GET"}, + RolesAllowed: []string{fakeTestRole, "test"}, + }, + UserContext: &userContext{ + roles: []string{fakeTestRole, fakeAdminRole}, + }, + }, + } + + for i, c := range tests { + if c.Resource != nil { + c.Context.Set(cxEnforce, c.Resource) + } + if c.UserContext != nil { + c.Context.Set(userContextName, c.UserContext) + } + handler(c.Context) + if c.Context.Writer.Status() != c.HTTPCode { + t.Errorf("test case %d should have recieved code: %d, got %d", i, c.HTTPCode, c.Context.Writer.Status()) + } + } +} diff --git a/resource_test.go b/resource_test.go index 86b674925a5939719ea34499902994913d2af7a5..641890f52ea8cc95f24efa1cd947475e343c067e 100644 --- a/resource_test.go +++ b/resource_test.go @@ -51,6 +51,15 @@ func TestIsValid(t *testing.T) { } } +func TestResourceString(t *testing.T) { + resource := &Resource{ + RolesAllowed: []string{"1", "2", "3"}, + } + if s := resource.String(); s == "" { + t.Errorf("we should have recieved a string") + } +} + func TestGetRoles(t *testing.T) { resource := &Resource{ RolesAllowed: []string{"1", "2", "3"}, diff --git a/server_test.go b/server_test.go index 3d5ec9bfa037d99227ad3b96d9e9c239a043cbb6..c5a7e049232aa20fb6518616e282adb00e1cbcb3 100644 --- a/server_test.go +++ b/server_test.go @@ -16,25 +16,145 @@ limitations under the License. package main import ( + "bufio" + "io/ioutil" + "net" + "net/http" "testing" + + log "github.com/Sirupsen/logrus" + "github.com/gin-gonic/gin" + "net/url" +) + +const ( + fakeClientID = "test" + fakeSecret = fakeClientID + + fakeAdminRoleURL = "/admin" + fakeTestRoleURL = "/test_role" + fakeTestAdminRolesURL = "/test_admin_roles" + + fakeAdminRole = "role:admin" + fakeTestRole = "role:test" ) func newFakeKeycloakProxy(t *testing.T) *KeycloakProxy { + log.SetOutput(ioutil.Discard) return &KeycloakProxy{ config: &Config{ - DiscoveryURL: "127.0.0.1:", - ClientID: "test_client", - Secret: "test_secret", - EncryptionKey: "AgXa7xRcoClDEU0ZDSH4X0XhL5Qy2Z2j", - Scopes: []string{}, - RefreshSession: false, + DiscoveryURL: "127.0.0.1:", + ClientID: fakeClientID, + Secret: fakeSecret, + EncryptionKey: "AgXa7xRcoClDEU0ZDSH4X0XhL5Qy2Z2j", + SkipTokenVerification: true, + Scopes: []string{}, + RefreshSession: false, + ClaimsMatch: map[string]string{ + "aud": fakeClientID, + }, Resources: []*Resource{ &Resource{ - URL: "/protect", + URL: fakeAdminRoleURL, Methods: []string{"GET"}, - RolesAllowed: []string{"test_role"}, + RolesAllowed: []string{fakeAdminRole}, + }, + &Resource{ + URL: fakeTestRoleURL, + Methods: []string{"GET"}, + RolesAllowed: []string{fakeTestRole}, + }, + &Resource{ + URL: fakeTestAdminRolesURL, + Methods: []string{"GET"}, + RolesAllowed: []string{fakeAdminRole, fakeTestRole}, }, }, }, } } + +func TestRedirectToAuthorization(t *testing.T) { + context := newFakeGinContext("GET", "/admin") + proxy := newFakeKeycloakProxy(t) + + proxy.config.SkipTokenVerification = false + proxy.redirectToAuthorization(context) + if context.Writer.Status() != http.StatusTemporaryRedirect { + t.Errorf("we should have been given a temporary redirect") + } + + proxy.config.SkipTokenVerification = true + proxy.redirectToAuthorization(context) + if context.Writer.Status() != http.StatusForbidden { + t.Errorf("we should have been given a forbidden code") + } +} + +func TestRedirectURL(t *testing.T) { + context := newFakeGinContext("GET", "/admin") + proxy := newFakeKeycloakProxy(t) + + if proxy.redirectToURL("http://127.0.0.1", context); context.Writer.Status() != http.StatusTemporaryRedirect { + t.Errorf("we should have recieved a redirect") + } + + if !context.IsAborted() { + t.Errorf("the context should have been aborted") + } +} + +func TestAccessForbidden(t *testing.T) { + context := newFakeGinContext("GET", "/admin") + proxy := newFakeKeycloakProxy(t) + + proxy.config.SkipTokenVerification = false + if proxy.accessForbidden(context); context.Writer.Status() != http.StatusForbidden { + t.Errorf("we should have recieved a forbidden access") + } + + proxy.config.SkipTokenVerification = true + if proxy.accessForbidden(context); context.Writer.Status() != http.StatusForbidden { + t.Errorf("we should have recieved a forbidden access") + } +} + +func newFakeResponse() *fakeResponse { + return &fakeResponse{ + status: http.StatusOK, + headers: make(http.Header, 0), + } +} + +func newFakeGinContext(method, uri string) *gin.Context { + return &gin.Context{ + Request: &http.Request{ + Method: method, + RequestURI: uri, + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1", + Path: "uri", + }, + }, + Writer: newFakeResponse(), + } +} + +type fakeResponse struct { + size int + status int + headers http.Header +} + +func (r *fakeResponse) Flush() {} +func (r *fakeResponse) Written() bool { return false } +func (r *fakeResponse) WriteHeaderNow() {} +func (r *fakeResponse) Size() int { return r.size } +func (r *fakeResponse) Status() int { return r.status } +func (r *fakeResponse) Header() http.Header { return r.headers } +func (r *fakeResponse) WriteHeader(code int) { r.status = code } +func (r *fakeResponse) Write(content []byte) (int, error) { return len(content), nil } +func (r *fakeResponse) WriteString(s string) (int, error) { return len(s), nil } +func (r *fakeResponse) Hijack() (net.Conn, *bufio.ReadWriter, error) { return nil, nil, nil } +func (r *fakeResponse) CloseNotify() <-chan bool { return make(chan bool, 0) } diff --git a/user_context_test.go b/user_context_test.go index ae944739bdc1b0150fd09538b3e41ed4f0a97f87..11f909fec655db2a53c419c7c86dbaca95ff6c18 100644 --- a/user_context_test.go +++ b/user_context_test.go @@ -32,7 +32,7 @@ func TestIsAudience(t *testing.T) { } } -func TestGetROles(t *testing.T) { +func TestGetUserRoles(t *testing.T) { user := &userContext{ roles: []string{"1", "2", "3"}, } @@ -52,3 +52,12 @@ func TestIsExpired(t *testing.T) { t.Errorf("we should have been false") } } + +func TestIsBearerToken(t *testing.T) { + user := &userContext{ + bearerToken: true, + } + if !user.isBearerToken() { + t.Errorf("the bearer token should have been true") + } +}