Skip to content
Snippets Groups Projects
Commit fc086d25 authored by Rohith's avatar Rohith
Browse files

Merge pull request #15 from gambol99/wip

wip
parents cc613c65 c27fea47
No related branches found
No related tags found
No related merge requests found
...@@ -39,7 +39,7 @@ GLOBAL OPTIONS: ...@@ -39,7 +39,7 @@ GLOBAL OPTIONS:
--tls-private-key the path to the private key for TLS support --tls-private-key the path to the private key for TLS support
--scope [--scope option --scope option] a variable list of scopes requested when authenticating the user --scope [--scope option --scope option] a variable list of scopes requested when authenticating the user
--claim [--claim option --claim option] a series of key pair values which must match the claims in the token present e.g. aud=myapp, iss=http://example.com etcd --claim [--claim option --claim option] a series of key pair values which must match the claims in the token present e.g. aud=myapp, iss=http://example.com etcd
--resource [--resource option --resource option] a list of resources 'uri=/admin|methods=GET|roles=role1,role2' --resource [--resource option --resource option] a list of resources 'uri=/admin|methods=GET|roles=role1,role2|whitelisted=(true|false)'
--signin-page a custom template displayed for signin --signin-page a custom template displayed for signin
--forbidden-page a custom template used for access forbidden --forbidden-page a custom template used for access forbidden
--tag [--tag option --tag option] a keypair tag which is passed to the templates when render, i.e. title='My Page',site='my name' etc --tag [--tag option --tag option] a keypair tag which is passed to the templates when render, i.e. title='My Page',site='my name' etc
...@@ -179,5 +179,26 @@ passed into the scope hold the oauth redirection url. If you wish pass additiona ...@@ -179,5 +179,26 @@ passed into the scope hold the oauth redirection url. If you wish pass additiona
</body> </body>
</html> </html>
#### **White-listed URL's**
Depending on how the application urls are laid out, you might want protect the root / url but have exceptions on a list of paths, i.e. /health etc. Although you should probably
fix this by fixing up the paths, you can add excepts to the protected resources. (Note: it's an array, so the order is important)
```YAML
resources:
- url: /some_white_listed_url
white-listed: true
- url: /
methods:
- GET
roles_allowed:
- <CLIENT_APP_NAME>:<ROLE_NAME>
- <CLIENT_APP_NAME>:<ROLE_NAME>
```
Or on the command line
```shell
--resource "uri=/some_white_listed_url,white-listed=true"
--resource "uri=/" # requires authentication on the rest
``` ```
...@@ -17,17 +17,12 @@ package main ...@@ -17,17 +17,12 @@ package main
import ( import (
"errors" "errors"
"net/http/httputil"
"net/url"
"time" "time"
"github.com/gambol99/go-oidc/oidc"
"github.com/gin-gonic/gin"
) )
const ( const (
prog = "keycloak-proxy" prog = "keycloak-proxy"
version = "v0.0.7" version = "v0.0.8"
author = "Rohith" author = "Rohith"
email = "gambol99@gmail.com" email = "gambol99@gmail.com"
description = "is a proxy using the keycloak service for auth and authorization" description = "is a proxy using the keycloak service for auth and authorization"
...@@ -66,6 +61,8 @@ type Resource struct { ...@@ -66,6 +61,8 @@ type Resource struct {
URL string `json:"url" yaml:"url"` URL string `json:"url" yaml:"url"`
// Methods the method type // Methods the method type
Methods []string `json:"methods" yaml:"methods"` Methods []string `json:"methods" yaml:"methods"`
// WhiteListed permits the prefix through
WhiteListed bool `json:"white-listed" yaml:"white-listed"`
// RolesAllowed the roles required to access this url // RolesAllowed the roles required to access this url
RolesAllowed []string `json:"roles_allowed" yaml:"roles_allowed"` RolesAllowed []string `json:"roles_allowed" yaml:"roles_allowed"`
} }
...@@ -119,26 +116,3 @@ type Config struct { ...@@ -119,26 +116,3 @@ type Config struct {
// Hostname is a list of hostnames the service should response to // Hostname is a list of hostnames the service should response to
Hostnames []string `json:"hostnames" yaml:"hostnames"` Hostnames []string `json:"hostnames" yaml:"hostnames"`
} }
// KeycloakProxy is the server component
type KeycloakProxy struct {
config *Config
// the gin service
router *gin.Engine
// the oidc provider config
openIDConfig oidc.ClientConfig
// the oidc client
openIDClient *oidc.Client
// the proxy client
proxy *httputil.ReverseProxy
// the upstream endpoint
upstreamURL *url.URL
}
// sessionState holds the state related data
type sessionState struct {
// the max time the session is permitted
expireOn time.Time
// the refresh token if any
refreshToken string
}
...@@ -95,6 +95,10 @@ func (r *KeycloakProxy) entrypointHandler() gin.HandlerFunc { ...@@ -95,6 +95,10 @@ func (r *KeycloakProxy) entrypointHandler() gin.HandlerFunc {
// step: check if authentication is required - gin doesn't support wildcard url, so we have have to use prefixes // step: check if authentication is required - gin doesn't support wildcard url, so we have have to use prefixes
for _, resource := range r.config.Resources { for _, resource := range r.config.Resources {
if strings.HasPrefix(cx.Request.RequestURI, resource.URL) { if strings.HasPrefix(cx.Request.RequestURI, resource.URL) {
// step: has the resource been white listed?
if resource.WhiteListed {
break
}
// step: inject the resource into the context, saves us from doing this again // step: inject the resource into the context, saves us from doing this again
if containedIn(cx.Request.Method, resource.Methods) || containedIn("ANY", resource.Methods) { if containedIn(cx.Request.Method, resource.Methods) || containedIn("ANY", resource.Methods) {
cx.Set(cxEnforce, resource) cx.Set(cxEnforce, resource)
......
...@@ -43,16 +43,26 @@ func TestEntrypointHandler(t *testing.T) { ...@@ -43,16 +43,26 @@ func TestEntrypointHandler(t *testing.T) {
{ {
Context: newFakeGinContext("GET", "/not_secure"), Context: newFakeGinContext("GET", "/not_secure"),
}, },
{
Context: newFakeGinContext("GET", fakeTestWhitelistedURL),
},
{ {
Context: newFakeGinContext("GET", oauthURL), Context: newFakeGinContext("GET", oauthURL),
}, },
{
Context: newFakeGinContext("GET", faketestListenOrdered), Secure: true,
},
} }
for i, c := range tests { for i, c := range tests {
handler(c.Context) handler(c.Context)
if _, found := c.Context.Get(cxEnforce); c.Secure && !found { _, found := c.Context.Get(cxEnforce)
if c.Secure && !found {
t.Errorf("test case %d should have been set secure", i) t.Errorf("test case %d should have been set secure", i)
} }
if !c.Secure && found {
t.Errorf("test case %d should not have been set secure", i)
}
} }
} }
...@@ -132,6 +142,18 @@ func TestAdmissionHandler(t *testing.T) { ...@@ -132,6 +142,18 @@ func TestAdmissionHandler(t *testing.T) {
roles: []string{fakeTestRole, fakeAdminRole}, roles: []string{fakeTestRole, fakeAdminRole},
}, },
}, },
{
Context: newFakeGinContext("POST", fakeAdminRoleURL),
HTTPCode: http.StatusForbidden,
Resource: &Resource{
URL: fakeAdminRoleURL,
Methods: []string{"POST"},
RolesAllowed: []string{fakeTestRole, "test"},
},
UserContext: &userContext{
roles: []string{fakeTestRole, fakeAdminRole},
},
},
} }
for i, c := range tests { for i, c := range tests {
...@@ -147,3 +169,12 @@ func TestAdmissionHandler(t *testing.T) { ...@@ -147,3 +169,12 @@ func TestAdmissionHandler(t *testing.T) {
} }
} }
} }
func TestHealthHandler(t *testing.T) {
proxy := newFakeKeycloakProxy(t)
context := newFakeGinContext("GET", healthURL)
proxy.healthHandler(context)
if context.Writer.Status() != http.StatusOK {
t.Errorf("we should have recieved a 200 response")
}
}
...@@ -59,6 +59,10 @@ func (r Resource) String() string { ...@@ -59,6 +59,10 @@ func (r Resource) String() string {
var roles string var roles string
var methods string var methods string
if r.WhiteListed {
return fmt.Sprintf("uri: %s, white-listed", r.URL)
}
if len(r.RolesAllowed) <= 0 { if len(r.RolesAllowed) <= 0 {
roles = "authentication only" roles = "authentication only"
} else { } else {
......
...@@ -18,14 +18,31 @@ package main ...@@ -18,14 +18,31 @@ package main
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httputil"
"net/url" "net/url"
"os" "os"
"sync" "sync"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/gambol99/go-oidc/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// KeycloakProxy is the server component
type KeycloakProxy struct {
config *Config
// the gin service
router *gin.Engine
// the oidc provider config
openIDConfig oidc.ClientConfig
// the oidc client
openIDClient *oidc.Client
// the proxy client
proxy *httputil.ReverseProxy
// the upstream endpoint
upstreamURL *url.URL
}
// newKeycloakProxy create's a new keycloak proxy from configuration // newKeycloakProxy create's a new keycloak proxy from configuration
func newKeycloakProxy(cfg *Config) (*KeycloakProxy, error) { func newKeycloakProxy(cfg *Config) (*KeycloakProxy, error) {
// step: set the logging level // step: set the logging level
......
...@@ -34,6 +34,9 @@ const ( ...@@ -34,6 +34,9 @@ const (
fakeAdminRoleURL = "/admin" fakeAdminRoleURL = "/admin"
fakeTestRoleURL = "/test_role" fakeTestRoleURL = "/test_role"
fakeTestAdminRolesURL = "/test_admin_roles" fakeTestAdminRolesURL = "/test_admin_roles"
fakeAuthAllURL = "/auth_all"
fakeTestWhitelistedURL = fakeAuthAllURL + "/white_listed"
faketestListenOrdered = fakeAuthAllURL + "/bad_order"
fakeAdminRole = "role:admin" fakeAdminRole = "role:admin"
fakeTestRole = "role:test" fakeTestRole = "role:test"
...@@ -69,6 +72,23 @@ func newFakeKeycloakProxy(t *testing.T) *KeycloakProxy { ...@@ -69,6 +72,23 @@ func newFakeKeycloakProxy(t *testing.T) *KeycloakProxy {
Methods: []string{"GET"}, Methods: []string{"GET"},
RolesAllowed: []string{fakeAdminRole, fakeTestRole}, RolesAllowed: []string{fakeAdminRole, fakeTestRole},
}, },
{
URL: fakeTestWhitelistedURL,
WhiteListed: true,
Methods: []string{},
RolesAllowed: []string{},
},
{
URL: fakeAuthAllURL,
Methods: []string{"ANY"},
RolesAllowed: []string{},
},
{
URL: fakeTestWhitelistedURL,
WhiteListed: true,
Methods: []string{},
RolesAllowed: []string{},
},
}, },
}, },
} }
......
...@@ -35,6 +35,14 @@ const ( ...@@ -35,6 +35,14 @@ const (
claimResourceRoles = "roles" claimResourceRoles = "roles"
) )
// sessionState holds the state related data
type sessionState struct {
// the max time the session is permitted
expireOn time.Time
// the refresh token if any
refreshToken string
}
// refreshUserSessionToken is responsible for retrieving the session state cookie and attempting to // refreshUserSessionToken is responsible for retrieving the session state cookie and attempting to
// refresh the access token for the user // refresh the access token for the user
func (r *KeycloakProxy) refreshUserSessionToken(cx *gin.Context) (jose.JWT, error) { func (r *KeycloakProxy) refreshUserSessionToken(cx *gin.Context) (jose.JWT, error) {
......
...@@ -235,6 +235,14 @@ func TestDecodeResource(t *testing.T) { ...@@ -235,6 +235,14 @@ func TestDecodeResource(t *testing.T) {
Methods: []string{"GET", "POST"}, Methods: []string{"GET", "POST"},
}, },
}, },
{
Option: "uri=/allow_me|white-listed=true",
Ok: true,
Resource: &Resource{
URL: "/allow_me",
WhiteListed: true,
},
},
{ {
Option: "", Option: "",
}, },
......
...@@ -285,7 +285,7 @@ func decodeResource(v string) (*Resource, error) { ...@@ -285,7 +285,7 @@ func decodeResource(v string) (*Resource, error) {
// step: split up the keypair // step: split up the keypair
kp := strings.Split(x, "=") kp := strings.Split(x, "=")
if len(kp) != 2 { if len(kp) != 2 {
return nil, fmt.Errorf("invalid resource keypair, should be (uri|roles|method)=comma_values") return nil, fmt.Errorf("invalid resource keypair, should be (uri|roles|method|white-listed)=comma_values")
} }
switch kp[0] { switch kp[0] {
case "uri": case "uri":
...@@ -294,6 +294,12 @@ func decodeResource(v string) (*Resource, error) { ...@@ -294,6 +294,12 @@ func decodeResource(v string) (*Resource, error) {
resource.Methods = strings.Split(kp[1], ",") resource.Methods = strings.Split(kp[1], ",")
case "roles": case "roles":
resource.RolesAllowed = strings.Split(kp[1], ",") resource.RolesAllowed = strings.Split(kp[1], ",")
case "white-listed":
value, err := strconv.ParseBool(kp[1])
if err != nil {
return nil, fmt.Errorf("the value of whitelisted must be true|TRUE|T or it's false equivilant")
}
resource.WhiteListed = value
default: default:
return nil, fmt.Errorf("invalid identifier, should be roles, uri or methods") return nil, fmt.Errorf("invalid identifier, should be roles, uri or methods")
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment