diff --git a/controller/tenant.go b/controller/tenant.go index 6f85e4f..85d10e6 100644 --- a/controller/tenant.go +++ b/controller/tenant.go @@ -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, } } @@ -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, @@ -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, @@ -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 @@ -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 +} diff --git a/controller/tenant_test.go b/controller/tenant_test.go new file mode 100644 index 0000000..4199a35 --- /dev/null +++ b/controller/tenant_test.go @@ -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) +} diff --git a/controller/tenants_test.go b/controller/tenants_test.go index c84f224..36907c5 100644 --- a/controller/tenants_test.go +++ b/controller/tenants_test.go @@ -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 @@ -68,7 +68,7 @@ func (s *TenantControllerTestSuite) TestShowTenants() { }) } -func (s *TenantControllerTestSuite) TestSearchTenants() { +func (s *TenantsControllerTestSuite) TestSearchTenants() { // given svc := goa.New("Tenants-service") diff --git a/glide.lock b/glide.lock index 5c73877..6ff31a6 100644 --- a/glide.lock +++ b/glide.lock @@ -1,26 +1,36 @@ -hash: 11d7ed58d948b60118aa58f7e7d2d71d8f2d231f507c2d3d3d318bf6dcbab9c7 -updated: 2017-11-12T00:05:38.194370944+01:00 +hash: 53a4be18320fbb006aa2a57986954133ce3b23530175f00c56345e4ae4a22cb6 +updated: 2018-01-16T15:43:47.65835094+01:00 imports: - name: github.com/ajg/form - version: cc2954064ec9ea8d93917f0f87456e11d7b881ad + version: 523a5da1a92f01b01f840b61689c0340a0243532 - name: github.com/armon/go-metrics version: 9a4b6e10bed6220a1665955aa2b75afc91eb10b3 - name: github.com/bitly/go-simplejson version: aabad6e819789e569bd6aabf444c935aa9ba1e44 +- name: github.com/davecgh/go-spew + version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 + subpackages: + - spew - name: github.com/dgrijalva/jwt-go version: dbeaa9332f19a944acb5736b4456cfcc02140e29 + subpackages: + - request - name: github.com/dimfeld/httppath - version: c8e499c3ef3c3e272ed8bdcc1ccf39f73c88debc + version: ee938bf735983d53694d79138ad9820efff94c92 - name: github.com/dimfeld/httptreemux version: 13dde8a00d96b369e7398490fd8a3af9ca114b84 +- name: github.com/dnaeon/go-vcr + version: 87d4990451a858cc210399285be976e63bc3c364 + subpackages: + - cassette + - recorder - name: github.com/elazarl/go-bindata-assetfs version: 9a6736ed45b44bf3835afeebb3034b57ed329f3e subpackages: - go-bindata-assetfs - name: github.com/fabric8-services/fabric8-auth - version: 26e19df61b885a92774632f23bb5590c5f768a31 + version: 388679243bf1a62bfaf943210bd5d0e0db48ebe5 subpackages: - - configuration - design - errors - log @@ -33,13 +43,14 @@ imports: - configuration - errors - goamiddleware + - goasupport - jsonapi - log - login/token_context - resource - rest - name: github.com/fsnotify/fsnotify - version: 629574ca2a5df945712d3079857300b5e4da0236 + version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9 - name: github.com/fzipp/gocyclo version: 6acd4345c835499920e8426c7e4e8d7a34f1bb83 - name: github.com/goadesign/goa @@ -67,6 +78,8 @@ imports: version: c7bacac2b21ca01afa1dee0acf64df3ce047c28f subpackages: - golint +- name: github.com/google/go-querystring + version: 9235644dd9e52eeae6fa48efd539fdc351a0af53 - name: github.com/hashicorp/go-immutable-radix version: 8aac2701530899b64bdea735a1de8da899815220 - name: github.com/hashicorp/golang-lru @@ -74,7 +87,7 @@ imports: subpackages: - simplelru - name: github.com/hashicorp/hcl - version: 37ab263305aaeb501a60eb16863e808d426e37f2 + version: 23c074d0eceb2b8a5bfdbb271ab780cde70f05a8 subpackages: - hcl/ast - hcl/parser @@ -97,60 +110,72 @@ imports: subpackages: - oid - name: github.com/magiconair/properties - version: be5ece7dd465ab0765a9682137865547526d1dfb + version: d419a98cdbed11a922bf76f257b7c4be79b50e73 - name: github.com/manveru/faker - version: 717f7cf83fb78669bfab612749c2e8ff63d5be11 + version: 9fbc68a78c4dbc7914e1a23f88f126bea4383b97 - name: github.com/mitchellh/mapstructure - version: 5a0325d7fafaac12dda6e7fb8bd222ec1b69875e + version: 06020f85339e21b2478f756a78e295255ffa4d6a - name: github.com/pelletier/go-buffruneio version: c37440a7cf42ac63b919c752ca73a85067e05992 - name: github.com/pelletier/go-toml version: 13d49d4606eb801b8f01ae542b4afc4c6ee3d84a - name: github.com/pkg/errors version: 645ef00459ed84a119197bfb8d8205042c6df63d +- name: github.com/pmezard/go-difflib + version: d8ed2627bdf02c080bf22230dbb337003b7aba2d + subpackages: + - difflib - name: github.com/satori/go.uuid - version: b061729afc07e77a8aa4fad0a2fd840958f1942a + version: 5bf94b69c6b68ee1b541973bb8e1144db23a194b - name: github.com/sirupsen/logrus - version: c078b1e43f58d563c74cebe63c85789e76ddb627 + version: 95cd2b9c79aa5e72ab0bc69b7ccc2be15bf850f6 - name: github.com/spf13/afero - version: 2f30b2a92c0e5700bcfe4715891adb1f2a7a406d + version: 8d919cbe7e2627e417f3e45c3c0e489a5b7e2536 subpackages: - mem - name: github.com/spf13/cast - version: 24b6558033ffe202bf42f0f3b870dcc798dd2ba8 + version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 - name: github.com/spf13/cobra - version: 9495bc009a56819bdb0ddbc1a373e29c140bc674 + version: 1be1d2841c773c01bee8289f55f7463b6e2c2539 - name: github.com/spf13/jwalterweatherman - version: 33c24e77fb80341fe7130ee7c594256ff08ccc46 + version: 12bd96e66386c1960ab0f74ced1362f66f552f7b - name: github.com/spf13/pflag - version: 5ccb023bc27df288a957c5e994cd44fd19619465 + version: 4c012f6dcd9546820e378d0bdda4d8fc772cdfea - name: github.com/spf13/viper - version: 651d9d916abc3c3d6a91a12549495caba5edffd2 + version: 4dddf7c62e16bce5807744018f5b753bfe21bbd2 - name: github.com/stretchr/testify - version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0 + version: b91bfb9ebec76498946beb6af7c0230c7cc7ba6c subpackages: - assert - require + - suite - name: github.com/Unleash/unleash-client-go - version: 5e8fdebb43e3dd7a8d25c4614925122b06c2ede3 + version: be883ea1af714aff427d0668dbd4fc549d4f701c + subpackages: + - context + - internal/api + - internal/strategies + - strategy - name: github.com/zach-klippenstein/goregen version: 795b5e3961ea1912fde60af417ad85e86acc0d6a - name: golang.org/x/crypto - version: 81e90905daefcd6fd217b62423c0908922eadb30 + version: 9f005a07e0d31d45e6656d241bb5c0f2efd4bc94 subpackages: - ed25519 - ed25519/internal/edwards25519 + - ssh/terminal - name: golang.org/x/net - version: b1a2d6e8c8b5fc8f601ead62536f02a8e1b6217d + version: c7086645de248775cbf2373cf5ca4d2fa664b8c1 subpackages: - context - websocket - name: golang.org/x/sys - version: 478fcf54317e52ab69f40bb4c7a1520288d7f7ea + version: 82aafbf43bf885069dc71b7e7c2f9d7a614d47da subpackages: - unix + - windows - name: golang.org/x/text - version: 47a200a05c8b3fd1b698571caecbb68beb2611ec + version: 88f656faf3f37f690df1a32515b479415e1a6769 subpackages: - transform - unicode/norm @@ -164,17 +189,9 @@ imports: - cipher - json - name: gopkg.in/yaml.v2 - version: a5b47d31c556af34a302ce5d659e6fea44d90de0 + version: 287cf08546ab5e7e37d55a84f7ed3fd1db036de5 testImports: -- name: github.com/davecgh/go-spew - version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 - subpackages: - - spew - name: github.com/jstemmer/go-junit-report version: 15422cf504f9dc030386499a5c68053a38763e58 -- name: github.com/pmezard/go-difflib - version: d8ed2627bdf02c080bf22230dbb337003b7aba2d - subpackages: - - difflib - name: github.com/wadey/gocovmerge version: b5bfa59ec0adc420475f97f89b58045c721d761c diff --git a/glide.yaml b/glide.yaml index 7617ed1..f22fd0f 100644 --- a/glide.yaml +++ b/glide.yaml @@ -3,7 +3,7 @@ import: - package: github.com/dgrijalva/jwt-go version: ^3.0.0 - package: github.com/goadesign/goa - version: ^1.3.0 + version: 1.3.0 vcs: git subpackages: - client @@ -27,7 +27,7 @@ import: - resource - jsonapi - package: github.com/fabric8-services/fabric8-auth - version: 26e19df61b885a92774632f23bb5590c5f768a31 + version: 388679243bf1a62bfaf943210bd5d0e0db48ebe5 subpackages: - design - token @@ -64,6 +64,8 @@ import: - package: github.com/bitly/go-simplejson version: ^0.5.0 - package: github.com/google/go-querystring +- package: github.com/dnaeon/go-vcr + version: 87d4990451a858cc210399285be976e63bc3c364 testImport: - package: github.com/wadey/gocovmerge - package: github.com/jstemmer/go-junit-report diff --git a/main.go b/main.go index e73a7cc..a6de3c1 100644 --- a/main.go +++ b/main.go @@ -163,10 +163,10 @@ func main() { app.MountStatusController(service, statusCtrl) // Mount "tenant" controller - witURL := config.GetWitURL() + authURL := config.GetAuthURL() tenantService := tenant.NewDBService(db) - tenantCtrl := controller.NewTenantController(service, tenantService, keycloakConfig, openshiftConfig, templateVars, witURL) + tenantCtrl := controller.NewTenantController(service, tenantService, http.DefaultClient, keycloakConfig, openshiftConfig, templateVars, authURL) app.MountTenantController(service, tenantCtrl) tenantsCtrl := controller.NewTenantsController(service, tenantService) @@ -184,7 +184,7 @@ func main() { log.Logger().Infoln("UTC Build Time: ", controller.BuildTime) log.Logger().Infoln("UTC Start Time: ", controller.StartTime) log.Logger().Infoln("Dev mode: ", config.IsDeveloperModeEnabled()) - log.Logger().Infoln("WIT URL: ", witURL) + log.Logger().Infoln("Auth URL: ", authURL) http.Handle("/favicon.ico", http.NotFoundHandler()) http.Handle("/", service.Mux) diff --git a/openshift/config.go b/openshift/config.go index f44b9ef..f1fac98 100644 --- a/openshift/config.go +++ b/openshift/config.go @@ -25,24 +25,22 @@ func (c Config) WithToken(token string) Config { return Config{MasterURL: c.MasterURL, MasterUser: c.MasterUser, Token: token, HttpTransport: c.HttpTransport, TemplateDir: c.TemplateDir, MavenRepoURL: c.MavenRepoURL, TeamVersion: c.TeamVersion} } -func (c Config) WithUserSettings(cheVersion string, jenkinsVersion string, teamVersion string, mavenRepoURL string) Config { - if len(cheVersion) > 0 || len(jenkinsVersion) > 0 || len(teamVersion) > 0 || len(mavenRepoURL) > 0 { - copy := c - if cheVersion != "" { - copy.CheVersion = cheVersion - } - if jenkinsVersion != "" { - copy.JenkinsVersion = jenkinsVersion - } - if teamVersion != "" { - copy.TeamVersion = teamVersion - } - if mavenRepoURL != "" { - copy.MavenRepoURL = mavenRepoURL - } - return copy +// WithUserSettings overrides the user settings with the given values (if not nil). +func (c Config) WithUserSettings(cheVersion, jenkinsVersion, teamVersion, mavenRepoURL *string) Config { + copy := c + if cheVersion != nil { + copy.CheVersion = *cheVersion } - return c + if jenkinsVersion != nil { + copy.JenkinsVersion = *jenkinsVersion + } + if teamVersion != nil { + copy.TeamVersion = *teamVersion + } + if mavenRepoURL != nil { + copy.MavenRepoURL = *mavenRepoURL + } + return copy } func (c Config) GetLogCallback() LogCallback { diff --git a/test/data/auth_client/auth_get_user.yaml b/test/data/auth_client/auth_get_user.yaml new file mode 100644 index 0000000..7fb1924 --- /dev/null +++ b/test/data/auth_client/auth_get_user.yaml @@ -0,0 +1,177 @@ +--- +version: 1 +interactions: +- request: + method: GET + url: http://auth-tests/api/user + headers: + sub: ["internal_user_with_config"] # will be compared against the `sub` claim in the incoming request's token + response: + status: 200 OK + code: 200 + body: '{"data": { + "attributes": { + "bio": "", + "cluster": "https://api.starter-us-east-2.openshift.com", + "company": "", + "contextInformation": { + "recentContexts": [{ + "space": null, + "user": "7cbbca62-289a-42db-af59-ebe50d1b8a98" + }], + "tenantConfig": { + "cheVersion": "another-che-version", + "jenkinsVersion": "another-jenkins-version", + "mavenRepo": "another-maven-url", + "teamVersion": "another-team-version" + } + }, + "created-at": "2017-12-07T11:31:49.125136Z", + "email": "developer@redhat", + "fullName": "developer developer", + "identityID": "7cbbca62-289a-42db-af59-ebe50d1b8a98", + "imageURL": "https://www.gravatar.com/avatar/c2642d0b7dd3d68b1cfbc5cbc9476b9a.jpg", + "providerType": "kc", + "registrationCompleted": true, + "updated-at": "2017-12-07T11:34:01.147795Z", + "url": "", + "userID": "2893ac62-889a-4e38-801d-0b83ce547d06", + "username": "developer", + "featureLevel": "internal" + }, + "id": "7cbbca62-289a-42db-af59-ebe50d1b8a98", + "links": { + "related": "http://192.168.64.12:31000/api/users/7cbbca62-289a-42db-af59-ebe50d1b8a98", + "self": "http://192.168.64.12:31000/api/users/7cbbca62-289a-42db-af59-ebe50d1b8a98" + }, + "type": "identities" + } +}' +- request: + method: GET + url: http://auth-tests/api/user + headers: + sub: ["external_user_with_config"] # will be compared against the `sub` claim in the incoming request's token + response: + status: 200 OK + code: 200 + body: '{"data": { + "attributes": { + "bio": "", + "cluster": "https://api.starter-us-east-2.openshift.com", + "company": "", + "contextInformation": { + "recentContexts": [{ + "space": null, + "user": "7cbbca62-289a-42db-af59-ebe50d1b8a98" + }], + "tenantConfig": { + "cheVersion": "another-che-version", + "jenkinsVersion": "another-jenkins-version", + "mavenRepo": "another-maven-url", + "teamVersion": "another-team-version" + } + }, + "created-at": "2017-12-07T11:31:49.125136Z", + "email": "developer@redhat", + "fullName": "developer developer", + "identityID": "7cbbca62-289a-42db-af59-ebe50d1b8a98", + "imageURL": "https://www.gravatar.com/avatar/c2642d0b7dd3d68b1cfbc5cbc9476b9a.jpg", + "providerType": "kc", + "registrationCompleted": true, + "updated-at": "2017-12-07T11:34:01.147795Z", + "url": "", + "userID": "2893ac62-889a-4e38-801d-0b83ce547d06", + "username": "developer", + "featureLevel": "beta" + }, + "id": "7cbbca62-289a-42db-af59-ebe50d1b8a98", + "links": { + "related": "http://192.168.64.12:31000/api/users/7cbbca62-289a-42db-af59-ebe50d1b8a98", + "self": "http://192.168.64.12:31000/api/users/7cbbca62-289a-42db-af59-ebe50d1b8a98" + }, + "type": "identities" + } +}' +- request: + method: GET + url: http://auth-tests/api/user + headers: + sub: ["internal_user_without_config"] # will be compared against the `sub` claim in the incoming request's token + response: + status: 200 OK + code: 200 + body: '{ + "data": { + "attributes": { + "bio": "", + "cluster": "https://api.starter-us-east-2.openshift.com", + "company": "", + "contextInformation": { + "recentContexts": [{ + "space": null, + "user": "7cbbca62-289a-42db-af59-ebe50d1b8a98" + }] + }, + "created-at": "2017-12-07T11:31:49.125136Z", + "email": "developer@redhat", + "fullName": "developer developer", + "identityID": "7cbbca62-289a-42db-af59-ebe50d1b8a98", + "imageURL": "https://www.gravatar.com/avatar/c2642d0b7dd3d68b1cfbc5cbc9476b9a.jpg", + "providerType": "kc", + "registrationCompleted": true, + "updated-at": "2017-12-07T11:34:01.147795Z", + "url": "", + "userID": "2893ac62-889a-4e38-801d-0b83ce547d06", + "username": "developer", + "featureLevel": "internal" + }, + "id": "7cbbca62-289a-42db-af59-ebe50d1b8a98", + "links": { + "related": "http://192.168.64.12:31000/api/users/7cbbca62-289a-42db-af59-ebe50d1b8a98", + "self": "http://192.168.64.12:31000/api/users/7cbbca62-289a-42db-af59-ebe50d1b8a98" + }, + "type": "identities" + } +}' +- request: + method: GET + url: http://auth-tests/api/user + headers: + sub: ["external_user_without_config"] # will be compared against the `sub` claim in the incoming request's token + response: + status: 200 OK + code: 200 + body: '{ + "data": { + "attributes": { + "bio": "", + "cluster": "https://api.starter-us-east-2.openshift.com", + "company": "", + "contextInformation": { + "recentContexts": [{ + "space": null, + "user": "7cbbca62-289a-42db-af59-ebe50d1b8a98" + }] + }, + "created-at": "2017-12-07T11:31:49.125136Z", + "email": "developer@redhat", + "fullName": "developer developer", + "identityID": "7cbbca62-289a-42db-af59-ebe50d1b8a98", + "imageURL": "https://www.gravatar.com/avatar/c2642d0b7dd3d68b1cfbc5cbc9476b9a.jpg", + "providerType": "kc", + "registrationCompleted": true, + "updated-at": "2017-12-07T11:34:01.147795Z", + "url": "", + "userID": "2893ac62-889a-4e38-801d-0b83ce547d06", + "username": "developer", + "featureLevel": "beta" + }, + "id": "7cbbca62-289a-42db-af59-ebe50d1b8a98", + "links": { + "related": "http://192.168.64.12:31000/api/users/7cbbca62-289a-42db-af59-ebe50d1b8a98", + "self": "http://192.168.64.12:31000/api/users/7cbbca62-289a-42db-af59-ebe50d1b8a98" + }, + "type": "identities" + } +}' \ No newline at end of file diff --git a/test/private_key.pem b/test/private_key.pem new file mode 100644 index 0000000..e578839 --- /dev/null +++ b/test/private_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEjgIBAAKB/g6X0vYyRg1xVKo8OUACpvDOXIqgQ3pUa5m8uVT2j4uz/Lb8kt2A +SsiwXwAFTpL6BcEgXM/eMrhodqvLT/CyDPEC24sXmgeTklAvs/Tqvm0YVJSRLWdX +6mh88BgtK7uq63fKxGUYo9YmlT7F9ejJtdkTaQApo8oa/zxHC3i0APSlaY0zU1F9 +59QYStFVCK8Skn3cbODF/+SN1oxj3//t69sGnMWqfTllFcG7hD361wGp/PXI/n8U +1sVOe3rZEecHWwJF5kltLu6y7XyqHpPYrk8Qp+Fo1o3WfEcaG7P2JZVve35P+xYW +Bvm7ERsbqih5aSFJz8C3hKlwSQOEVtttAgMBAAECgf1c27yM4VriL0WP+6hQqI+h +wYEcnLDEumv22fB3tHe3gJeXzZq93p4AbEwW1a4nks8LG+N61W3qAtEgW5tTAalX +9dcOPiDkFSXzGZkD4Lnbefa7aRKBh+0S9fDT5ptikznGC32r0B65lMobp5IiuWds +6RY88rpKU3/PEETuzHtFO1kT/Dg/mny5FOTsmJjt+qjwROFCgiQBo/ZN33YTvF9v +aOcg5r2T3NmsDoXjorSHIFQ7+GC0Fj/rcUzLss9xgIsC93hJC1vt4+OlJSM4rIaW +GVI3fyVTZCjDqi2csXZCytTCSJRs1TggOo9jaine/xRi4r9jdXhmmyK9cXqhAn8+ +AzcItixuziv7evlsHUsTTV/RR5TA9K+yXNm+9elX9NsaSVj0kRQeqTGWBua1bn0l +m+pvlsYimupSpJ3gmcrbcHUUdqvj655LIinWrLlN3GMLQyCvljk9BLWIrFQ6godR +rWEq2Kdni0vlm8f3h9hdR2XgS+dOvUKzE9R16rnJAn88PjbXRtTwf0hRXFYGigf5 +DGYHt4WiIemduEC8Na+e2g6Sigdg7IqzQ04oSaWB+bM3tinyX+qIemaVL01VkJp4 +7DMQrDx+KTsmmtYzBu7YLahZA0TqsnNRGeG+O3htDKsSKVUSsczoXu1Ar1+3nNim +RppCNMJGisNKFqRbjKaFAn8BEu8uEHGejaWHWm7dZ3h4YhuptTKnUNWGIkOHIh0j +b9MnlmObALQ3f7ijH4V5WOuD7jpWKmdODB7IxZ8SV7eCq2TrsM5zSQ5ZwMK2vBEN +fyab+FKll9Vv8BfwwQNIbCBJ0tXe9xeXHHt5A4SoDcs6elUSWF4uJ+ryzQId9K25 +An8KMIe8H+nyh8TmphSS5JP2pwc29O6wfsXx/HFOpFIBL2bZmGkpFrlbGt5EaDiL +ZH3QxYoQyfJ0hSeGwkp1V5EZNPJqNofA2x57KCNk3B5YCFj6PVhRzj89D4CkWZDD ++SmSV9Vg5RwAjdXZZBBvkSL/9N8wpZXasqvXgz7nkUG1An8vOMeAmGunp9Be8r8Y +9QVTkvFtd44pLB/43sL6ZoXK0oPKgJUmCqPAYaJFRlfcdoHTQgymk0DuOKFhb4Ai +pfpBDkEIDzPInc3pWAtaqkZD7BdMnku3m78RNAOO97tMJPYQxxnNX16BqWm/SahW +chFXW3cvVxsaRJXr/a0gOYjI +-----END RSA PRIVATE KEY----- diff --git a/test/public_key.pem b/test/public_key.pem new file mode 100644 index 0000000..eae6f4c --- /dev/null +++ b/test/public_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBHjANBgkqhkiG9w0BAQEFAAOCAQsAMIIBBgKB/g6X0vYyRg1xVKo8OUACpvDO +XIqgQ3pUa5m8uVT2j4uz/Lb8kt2ASsiwXwAFTpL6BcEgXM/eMrhodqvLT/CyDPEC +24sXmgeTklAvs/Tqvm0YVJSRLWdX6mh88BgtK7uq63fKxGUYo9YmlT7F9ejJtdkT +aQApo8oa/zxHC3i0APSlaY0zU1F959QYStFVCK8Skn3cbODF/+SN1oxj3//t69sG +nMWqfTllFcG7hD361wGp/PXI/n8U1sVOe3rZEecHWwJF5kltLu6y7XyqHpPYrk8Q +p+Fo1o3WfEcaG7P2JZVve35P+xYWBvm7ERsbqih5aSFJz8C3hKlwSQOEVtttAgMB +AAE= +-----END PUBLIC KEY----- diff --git a/toggles/features.go b/toggles/features.go new file mode 100644 index 0000000..3ac6bf9 --- /dev/null +++ b/toggles/features.go @@ -0,0 +1,6 @@ +package toggles + +const ( + // UserUpdateTenantFeature the name of the feature that lets a user update his tenant config + UserUpdateTenantFeature string = "user.update-tenant" +) diff --git a/toggles/listener.go b/toggles/listener.go new file mode 100644 index 0000000..3180d98 --- /dev/null +++ b/toggles/listener.go @@ -0,0 +1,52 @@ +package toggles + +import ( + unleash "github.com/Unleash/unleash-client-go" + "github.com/fabric8-services/fabric8-wit/log" +) + +type clientListener struct { + client *Client +} + +// OnError prints out errors. +func (l clientListener) OnError(err error) { + log.Error(nil, map[string]interface{}{ + "err": err.Error(), + }, "toggles error") +} + +// OnWarning prints out warning. +func (l clientListener) OnWarning(warning error) { + log.Warn(nil, map[string]interface{}{ + "err": warning.Error(), + }, "toggles warning") +} + +// OnReady prints to the console when the repository is ready. +func (l clientListener) OnReady() { + l.client.ready = true + log.Info(nil, map[string]interface{}{}, "toggles ready") +} + +// OnCount prints to the console when the feature is queried. +func (l clientListener) OnCount(name string, enabled bool) { + log.Info(nil, map[string]interface{}{ + "name": name, + "enabled": enabled, + }, "toggles count") +} + +// OnSent prints to the console when the server has uploaded metrics. +func (l clientListener) OnSent(payload unleash.MetricsData) { + log.Info(nil, map[string]interface{}{ + "payload": payload, + }, "toggles sent") +} + +// OnRegistered prints to the console when the client has registered. +func (l clientListener) OnRegistered(payload unleash.ClientData) { + log.Info(nil, map[string]interface{}{ + "payload": payload, + }, "toggles registered") +} diff --git a/toggles/toggles.go b/toggles/toggles.go index d429284..11d7305 100644 --- a/toggles/toggles.go +++ b/toggles/toggles.go @@ -26,10 +26,15 @@ func Init(serviceName, hostURL string) { ) } -// WithContext creates a Token based contex +// WithContext creates a Token based context func WithContext(ctx context.Context) unleash.FeatureOption { - uctx := ucontext.Context{} token := goajwt.ContextJWT(ctx) + return WithToken(token) +} + +// WithToken creates a Token based context +func WithToken(token *jwt.Token) unleash.FeatureOption { + uctx := ucontext.Context{} if token != nil { if claims, ok := token.Claims.(jwt.MapClaims); ok { uctx.UserId = claims["sub"].(string) diff --git a/toggles/toggles_client_wrapper.go b/toggles/toggles_client_wrapper.go new file mode 100644 index 0000000..708b736 --- /dev/null +++ b/toggles/toggles_client_wrapper.go @@ -0,0 +1,70 @@ +package toggles + +import ( + "os" + "time" + + "github.com/Unleash/unleash-client-go" + jwt "github.com/dgrijalva/jwt-go" + "github.com/fabric8-services/fabric8-wit/log" +) + +// UnleashClient the interface for the underlying Unleash client +type UnleashClient interface { + Ready() <-chan bool + IsEnabled(feature string, options ...unleash.FeatureOption) (enabled bool) + Close() error +} + +// Client the wrapper for the toggle client +type Client struct { + ready bool + UnleashClient UnleashClient +} + +// NewClient returns a new client to the toggle feature service including the default underlying unleash client initialized +func NewClient(serviceName, hostURL string) (*Client, error) { + l := clientListener{} + unleashclient, err := unleash.NewClient( + unleash.WithAppName(serviceName), + unleash.WithInstanceId(os.Getenv("HOSTNAME")), + unleash.WithUrl(hostURL), + unleash.WithMetricsInterval(1*time.Minute), + unleash.WithRefreshInterval(10*time.Second), + unleash.WithListener(l), + ) + if err != nil { + return nil, err + } + result := NewCustomClient(unleashclient, false) + l.client = result + return result, nil +} + +// NewCustomClient returns a new client to the toggle feature service with a pre-initialized unleash client +func NewCustomClient(unleashclient UnleashClient, ready bool) *Client { + result := &Client{ + UnleashClient: unleashclient, + ready: ready, + } + return result +} + +// Close closes the underlying Unleash client +func (c *Client) Close() error { + return c.UnleashClient.Close() +} + +// Ready returns `true` if the client is ready +func (c *Client) Ready() bool { + return c.ready +} + +// IsEnabled returns a boolean to specify whether on feature is enabled given the user's context (in particular, his/her token claims) +func (c *Client) IsEnabled(token *jwt.Token, feature string, fallback bool) bool { + log.Info(nil, map[string]interface{}{"feature_name": feature, "ready": c.Ready()}, "checking if feature is enabled") + if !c.Ready() { + return fallback + } + return c.UnleashClient.IsEnabled(feature, WithToken(token), unleash.WithFallback(fallback)) +}