Skip to content
This repository has been archived by the owner on Mar 11, 2021. It is now read-only.

Use whitelist to restrict permission to user profile override #495

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 95 additions & 28 deletions controller/tenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,50 @@ import (
"context"
"fmt"
"net/http"
"strings"
"net/url"
"reflect"
"time"

"github.com/bitly/go-simplejson"
jwt "github.com/dgrijalva/jwt-go"
"github.com/fabric8-services/fabric8-tenant/app"
"github.com/fabric8-services/fabric8-tenant/auth"
"github.com/fabric8-services/fabric8-tenant/jsonapi"
"github.com/fabric8-services/fabric8-tenant/keycloak"
"github.com/fabric8-services/fabric8-tenant/openshift"
"github.com/fabric8-services/fabric8-tenant/tenant"
"github.com/fabric8-services/fabric8-wit/errors"
"github.com/fabric8-services/fabric8-wit/goasupport"
"github.com/fabric8-services/fabric8-wit/log"
"github.com/fabric8-services/fabric8-wit/rest"
"github.com/goadesign/goa"
goaclient "github.com/goadesign/goa/client"
goajwt "github.com/goadesign/goa/middleware/security/jwt"
errs "github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
)

// TenantController implements the status resource.
type TenantController struct {
*goa.Controller
httpClient *http.Client
tenantService tenant.Service
keycloakConfig keycloak.Config
openshiftConfig openshift.Config
templateVars map[string]string
usersURL string
authURL string
}

// NewTenantController creates a status controller.
func NewTenantController(service *goa.Service, tenantService tenant.Service, keycloakConfig keycloak.Config, openshiftConfig openshift.Config, templateVars map[string]string, usersURL string) *TenantController {
func NewTenantController(service *goa.Service, tenantService tenant.Service, httpClient *http.Client, keycloakConfig keycloak.Config, openshiftConfig openshift.Config, templateVars map[string]string, authURL string) *TenantController {
return &TenantController{
Controller: service.NewController("TenantController"),
httpClient: httpClient,
tenantService: tenantService,
keycloakConfig: keycloakConfig,
openshiftConfig: openshiftConfig,
templateVars: templateVars,
usersURL: usersURL,
authURL: authURL,
}
}

Expand Down Expand Up @@ -81,7 +88,7 @@ func (c *TenantController) Setup(ctx *app.SetupTenantContext) error {
return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError(ctx, err))
}

oc, err := c.loadUserTenantConfiguration(token, c.openshiftConfig)
oc, err := c.loadUserTenantConfiguration(ctx, c.openshiftConfig)
if err != nil {
log.Error(ctx, map[string]interface{}{
"err": err,
Expand Down Expand Up @@ -145,7 +152,7 @@ func (c *TenantController) Update(ctx *app.UpdateTenantContext) error {
if openshift.KubernetesMode() {
rawOC = &userConfig
}
oc, err := c.loadUserTenantConfiguration(token, *rawOC)
oc, err := c.loadUserTenantConfiguration(ctx, *rawOC)
if err != nil {
log.Error(ctx, map[string]interface{}{
"err": err,
Expand Down Expand Up @@ -175,34 +182,80 @@ func (c *TenantController) Update(ctx *app.UpdateTenantContext) error {
return ctx.Accepted()
}

func (c *TenantController) loadUserTenantConfiguration(token *jwt.Token, config openshift.Config) (openshift.Config, error) {
authHeader := token.Raw
if len(c.usersURL) > 0 {
url := strings.TrimSuffix(c.usersURL, "/") + "/user"
status, body, err := openshift.GetJSON(config, url, authHeader)
if err != nil {
return config, err
}
if status < 200 || status > 300 {
return config, fmt.Errorf("Failed to GET url %s due to status code %d", url, status)
}
js, err := simplejson.NewJson([]byte(body))
if err != nil {
return config, err
}
// loadUserTenantConfiguration loads the tenant configuration for `auth`,
// allowing for config overrides based on the content of his profile (in auth) if the user is allowed
func (c *TenantController) loadUserTenantConfiguration(ctx context.Context, config openshift.Config) (openshift.Config, error) {
// restrict access to users with a `featureLevel` set to `internal`
user, err := c.getCurrentUser(ctx)
if err != nil {
log.Error(ctx, map[string]interface{}{"auth_url": c.authURL}, "unable get current user")
return config, err
}

tenantConfig := js.GetPath("data", "attributes", "contextInformation", "tenantConfig")
if tenantConfig.Interface() != nil {
cheVersion := getJsonStringOrBlank(tenantConfig, "cheVersion")
jenkinsVersion := getJsonStringOrBlank(tenantConfig, "jenkinsVersion")
teamVersion := getJsonStringOrBlank(tenantConfig, "teamVersion")
mavenRepoURL := getJsonStringOrBlank(tenantConfig, "mavenRepo")
return config.WithUserSettings(cheVersion, jenkinsVersion, teamVersion, mavenRepoURL), nil
if user.Data.Attributes.FeatureLevel != nil && *user.Data.Attributes.FeatureLevel == "internal" {
log.Debug(ctx,
map[string]interface{}{
"auth_url": auth.ShowUserPath(),
"user_name": *user.Data.Attributes.Username,
"feature_level": *user.Data.Attributes.FeatureLevel},
"user is allowed to update tenant config")
if tenantConfig, exists := user.Data.Attributes.ContextInformation["tenantConfig"]; exists {
if tenantConfigMap, ok := tenantConfig.(map[string]interface{}); ok {
var cheVersion, jenkinsVersion, teamVersion, mavenRepoURL *string
if v, ok := tenantConfigMap["cheVersion"].(string); ok {
cheVersion = &v
}
if v, ok := tenantConfigMap["jenkinsVersion"].(string); ok {
jenkinsVersion = &v
}
if v, ok := tenantConfigMap["teamVersion"].(string); ok {
teamVersion = &v
}
if v, ok := tenantConfigMap["mavenRepo"].(string); ok {
mavenRepoURL = &v
}
return config.WithUserSettings(cheVersion, jenkinsVersion, teamVersion, mavenRepoURL), nil
}
}
} else {
curentFeatureLevel := "undefined"
if user.Data.Attributes.FeatureLevel != nil {
curentFeatureLevel = *user.Data.Attributes.FeatureLevel
}
log.Error(ctx,
map[string]interface{}{
"auth_url": auth.ShowUserPath(),
"user_name": *user.Data.Attributes.Username,
"feature_level": curentFeatureLevel},
"user is not allowed to update tenant config")
}
return config, nil
}

func (c *TenantController) getCurrentUser(ctx context.Context) (*auth.User, error) {
log.Info(ctx, map[string]interface{}{"auth_url": c.authURL, "http_client_transport": reflect.TypeOf(c.httpClient.Transport)}, "retrieving user's profile...")
authClient, err := newAuthClient(ctx, c.httpClient, c.authURL)
if err != nil {
log.Error(ctx, map[string]interface{}{"auth_url": c.authURL}, "unable to parse auth URL")
return nil, err
}
resp, err := authClient.ShowUser(ctx, auth.ShowUserPath(), nil, nil)
if err != nil {
log.Error(ctx, map[string]interface{}{"auth_url": auth.ShowUserPath()}, "unable to get user info")
return nil, errs.Wrapf(err, "failed to get user info on %s due to error", auth.ShowUserPath())
}
if resp.StatusCode < 200 || resp.StatusCode > 300 {
return nil, fmt.Errorf("failed to GET %s due to status code %d", resp.Request.URL, resp.StatusCode)
}
defer resp.Body.Close()
user, err := authClient.DecodeUser(resp)
if err != nil {
log.Error(ctx, map[string]interface{}{"auth_url": auth.ShowUserPath()}, "failed to decode user")
return nil, errs.Wrapf(err, "failed to decode user")
}
return user, nil
}

func getJsonStringOrBlank(json *simplejson.Json, key string) string {
text, _ := json.Get(key).String()
return text
Expand Down Expand Up @@ -424,3 +477,17 @@ func convertTenant(tenant *tenant.Tenant, namespaces []*tenant.Namespace) *app.T
}
return &result
}

// NewAuthClient initializes a new client to the `auth` service
func newAuthClient(ctx context.Context, httpClient *http.Client, authURL string) (*auth.Client, error) {
log.Info(ctx, map[string]interface{}{"auth_url": authURL, "http_client_transport": reflect.TypeOf(httpClient.Transport)}, "initializing a new auth client...")
u, err := url.Parse(authURL)
if err != nil {
return nil, err
}
c := auth.New(goaclient.HTTPClientDoer(httpClient))
c.Host = u.Host
c.Scheme = u.Scheme
c.SetJWTSigner(goasupport.NewForwardSigner(ctx))
return c, nil
}
169 changes: 169 additions & 0 deletions controller/tenant_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package controller

import (
"context"
"crypto/rsa"
"io/ioutil"
"log"
"net/http"
"testing"

jwt "github.com/dgrijalva/jwt-go"
jwtrequest "github.com/dgrijalva/jwt-go/request"
"github.com/dnaeon/go-vcr/cassette"
"github.com/dnaeon/go-vcr/recorder"
"github.com/fabric8-services/fabric8-tenant/keycloak"
"github.com/fabric8-services/fabric8-tenant/openshift"
"github.com/goadesign/goa"
goajwt "github.com/goadesign/goa/middleware/security/jwt"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

type TenantControllerTestSuite struct {
suite.Suite
}

func TestTenantController(t *testing.T) {
// resource.Require(t, resource.Database)
suite.Run(t, &TenantControllerTestSuite{})
}

func (s *TenantControllerTestSuite) TestLoadTenantConfiguration() {

// given
openshiftConfig := openshift.Config{
CheVersion: "che-version",
JenkinsVersion: "jenkins-version",
MavenRepoURL: "maven-url",
TeamVersion: "team-version",
}

s.T().Run("override disabled", func(t *testing.T) {

t.Run("external user with config", func(t *testing.T) {
// given
ctrl := newTenantController(t, openshiftConfig)
ctx := createValidContext(s.T(), "external_user_with_config")
// when
resultConfig, err := ctrl.loadUserTenantConfiguration(ctx, openshiftConfig)
// then
require.NoError(t, err)
assert.Equal(t, openshiftConfig, resultConfig)
})

t.Run("external user without config", func(t *testing.T) {
// given
ctrl := newTenantController(t, openshiftConfig)
ctx := createValidContext(s.T(), "external_user_without_config")
// when
resultConfig, err := ctrl.loadUserTenantConfiguration(ctx, openshiftConfig)
// then
require.NoError(t, err)
assert.Equal(t, openshiftConfig, resultConfig)
})
})

s.T().Run("override enabled", func(t *testing.T) {

t.Run("internal user with config", func(t *testing.T) {
// given
ctrl := newTenantController(t, openshiftConfig)
ctx := createValidContext(s.T(), "internal_user_with_config")
// when
resultConfig, err := ctrl.loadUserTenantConfiguration(ctx, openshiftConfig)
// then
require.NoError(t, err)
expectedOpenshiftConfig := openshift.Config{
CheVersion: "another-che-version",
JenkinsVersion: "another-jenkins-version",
MavenRepoURL: "another-maven-url",
TeamVersion: "another-team-version",
}
assert.Equal(t, expectedOpenshiftConfig, resultConfig)
})

t.Run("internal user without config", func(t *testing.T) {
// given
ctrl := newTenantController(t, openshiftConfig)
ctx := createValidContext(s.T(), "internal_user_without_config")
// when
resultConfig, err := ctrl.loadUserTenantConfiguration(ctx, openshiftConfig)
// then
require.NoError(t, err)
assert.Equal(t, openshiftConfig, resultConfig)
})
})

}

func newTenantController(t *testing.T, defaultConfig openshift.Config) *TenantController {
svc := goa.New("Tenants-service")
authURL := "http://auth-tests"
templateVars := make(map[string]string)
tenantService := mockTenantService{ID: uuid.NewV4()}
r, err := recorder.New("../test/data/auth_client/auth_get_user")
require.Nil(t, err)
defer r.Stop()
r.SetMatcher(func(httpRequest *http.Request, cassetteRequest cassette.Request) bool {
log.Println("comparing http request with cassette request....")
if httpRequest.URL != nil && httpRequest.URL.String() != cassetteRequest.URL {
log.Printf("Request URL does not match with cassette: %s vs %s\n", httpRequest.URL.String(), cassetteRequest.URL)
return false
}
if httpRequest.Method != cassetteRequest.Method {
log.Printf("Request Method does not match with cassette: %s vs %s\n", httpRequest.Method, cassetteRequest.Method)
return false
}

// look-up the JWT's "sub" claim and compare with the request
token, err := jwtrequest.ParseFromRequest(httpRequest, jwtrequest.AuthorizationHeaderExtractor, func(*jwt.Token) (interface{}, error) {
return PublicKey()
})
if err != nil {
log.Panic(nil, map[string]interface{}{"error": err.Error()}, "failed to parse token from request")
}
claims := token.Claims.(jwt.MapClaims)
if sub, found := cassetteRequest.Headers["sub"]; found {
log.Printf("Comparing subs: %s vs %s\n", sub[0], claims["sub"])
return sub[0] == claims["sub"]
}
log.Printf("Request token does not match with cassette")
return false
})
mockHTTPClient := &http.Client{
Transport: r,
}
return NewTenantController(svc, tenantService, mockHTTPClient, keycloak.Config{}, defaultConfig, templateVars, authURL)
}

func createValidContext(t *testing.T, userID string) context.Context {
claims := jwt.MapClaims{}
claims["sub"] = userID
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
// use the test private key to sign the token
key, err := PrivateKey()
require.NoError(t, err)
signed, err := token.SignedString(key)
require.NoError(t, err)
token.Raw = signed
return goajwt.WithJWT(context.Background(), token)
}

func PrivateKey() (*rsa.PrivateKey, error) {
rsaPrivateKey, err := ioutil.ReadFile("../test/private_key.pem")
if err != nil {
return nil, err
}
return jwt.ParseRSAPrivateKeyFromPEM(rsaPrivateKey)
}

func PublicKey() (*rsa.PublicKey, error) {
rsaPublicKey, err := ioutil.ReadFile("../test/public_key.pem")
if err != nil {
return nil, err
}
return jwt.ParseRSAPublicKeyFromPEM(rsaPublicKey)
}
10 changes: 5 additions & 5 deletions controller/tenants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ import (
"github.com/stretchr/testify/suite"
)

type TenantControllerTestSuite struct {
type TenantsControllerTestSuite struct {
gormsupport.DBTestSuite
}

func TestTenantController(t *testing.T) {
func TestTenantsController(t *testing.T) {
resource.Require(t, resource.Database)
suite.Run(t, &TenantControllerTestSuite{DBTestSuite: gormsupport.NewDBTestSuite("../config.yaml")})
suite.Run(t, &TenantsControllerTestSuite{DBTestSuite: gormsupport.NewDBTestSuite("../config.yaml")})
}

func (s *TenantControllerTestSuite) TestShowTenants() {
func (s *TenantsControllerTestSuite) TestShowTenants() {

s.T().Run("OK", func(t *testing.T) {
// given
Expand Down Expand Up @@ -68,7 +68,7 @@ func (s *TenantControllerTestSuite) TestShowTenants() {
})
}

func (s *TenantControllerTestSuite) TestSearchTenants() {
func (s *TenantsControllerTestSuite) TestSearchTenants() {
// given
svc := goa.New("Tenants-service")

Expand Down
Loading