diff --git a/api/cmd/main.go b/api/cmd/main.go index a13f970f..46725818 100644 --- a/api/cmd/main.go +++ b/api/cmd/main.go @@ -3,10 +3,12 @@ package main import ( "encoding/json" "fmt" + "math" "net/http" "os" "path/filepath" "strings" + "time" "github.com/gorilla/mux" "github.com/heptiolabs/healthcheck" @@ -18,6 +20,7 @@ import ( "github.com/caraml-dev/mlp/api/config" "github.com/caraml-dev/mlp/api/database" "github.com/caraml-dev/mlp/api/log" + "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" ) func main() { @@ -65,7 +68,9 @@ func main() { SentryDSN: cfg.SentryDSN, Streams: cfg.Streams, Docs: cfg.Docs, - UIConfig: cfg.UI, + MaxAuthzCacheExpiryMinutes: fmt.Sprintf("%.0f", + math.Ceil((time.Duration(enforcer.MaxKeyExpirySeconds) * time.Second).Minutes())), + UIConfig: cfg.UI, } router.Methods("GET").Path("/env.js").HandlerFunc(uiEnv.handler) @@ -89,12 +94,13 @@ func mount(r *mux.Router, path string, handler http.Handler) { type uiEnvHandler struct { *config.UIConfig - APIURL string `json:"REACT_APP_API_URL,omitempty"` - OauthClientID string `json:"REACT_APP_OAUTH_CLIENT_ID,omitempty"` - Environment string `json:"REACT_APP_ENVIRONMENT,omitempty"` - SentryDSN string `json:"REACT_APP_SENTRY_DSN,omitempty"` - Streams config.Streams `json:"REACT_APP_STREAMS"` - Docs config.Documentations `json:"REACT_APP_DOC_LINKS"` + APIURL string `json:"REACT_APP_API_URL,omitempty"` + OauthClientID string `json:"REACT_APP_OAUTH_CLIENT_ID,omitempty"` + Environment string `json:"REACT_APP_ENVIRONMENT,omitempty"` + SentryDSN string `json:"REACT_APP_SENTRY_DSN,omitempty"` + Streams config.Streams `json:"REACT_APP_STREAMS"` + Docs config.Documentations `json:"REACT_APP_DOC_LINKS"` + MaxAuthzCacheExpiryMinutes string `json:"REACT_APP_MAX_AUTHZ_CACHE_EXPIRY_MINUTES"` } func (h uiEnvHandler) handler(w http.ResponseWriter, r *http.Request) { diff --git a/api/config/config.go b/api/config/config.go index a1f579ca..97e25e5c 100644 --- a/api/config/config.go +++ b/api/config/config.go @@ -190,9 +190,9 @@ var defaultConfig = &Config{ Environment: "dev", Port: 8080, - Streams: Streams{}, - Docs: Documentations{}, - + Streams: Streams{}, + Docs: Documentations{}, + Applications: []modelsv2.Application{}, Authorization: &AuthorizationConfig{ Enabled: false, Caching: &InMemoryCacheConfig{ diff --git a/api/config/config_test.go b/api/config/config_test.go index 454a6636..def4dd49 100644 --- a/api/config/config_test.go +++ b/api/config/config_test.go @@ -76,8 +76,9 @@ func TestLoad(t *testing.T) { MaxIdleConns: 10, MaxOpenConns: 20, }, - Mlflow: &config.MlflowConfig{}, - Docs: []config.Documentation{}, + Mlflow: &config.MlflowConfig{}, + Docs: []config.Documentation{}, + Applications: []modelsv2.Application{}, Streams: map[string][]string{ "stream-1": {"team-a", "team-b"}, "SecondStream": {"MyTeam"}, @@ -132,8 +133,9 @@ func TestLoad(t *testing.T) { MaxIdleConns: 10, MaxOpenConns: 20, }, - Mlflow: &config.MlflowConfig{}, - Docs: []config.Documentation{}, + Mlflow: &config.MlflowConfig{}, + Docs: []config.Documentation{}, + Applications: []modelsv2.Application{}, Streams: map[string][]string{ "stream-1": {"team-a", "team-b"}, "SecondStream": {"MyTeam"}, diff --git a/api/pkg/authz/enforcer/enforcer.go b/api/pkg/authz/enforcer/enforcer.go index 8a96e32a..131c4664 100644 --- a/api/pkg/authz/enforcer/enforcer.go +++ b/api/pkg/authz/enforcer/enforcer.go @@ -48,6 +48,9 @@ type CacheConfig struct { CacheCleanUpIntervalSeconds int } +// MaxKeyExpirySeconds is the max allowed value for the KeyExpirySeconds. +const MaxKeyExpirySeconds = 600 + // Enforcer thin client providing interface for authorizing users type Enforcer interface { // Enforce check whether user is authorized to do certain action against a resource @@ -84,7 +87,7 @@ func newEnforcer( flavor Flavor, timeout time.Duration, cacheConfig *CacheConfig, -) (Enforcer, error) { +) (*enforcer, error) { u, err := url.ParseRequestURI(hostURL) if err != nil { return nil, err @@ -102,6 +105,10 @@ func newEnforcer( timeout: timeout, } if cacheConfig != nil { + if cacheConfig.KeyExpirySeconds > MaxKeyExpirySeconds { + return nil, fmt.Errorf("Configured KeyExpirySeconds is larger than the max permitted value of %d", + MaxKeyExpirySeconds) + } enforcer.cache = newInMemoryCache(cacheConfig.KeyExpirySeconds, cacheConfig.CacheCleanUpIntervalSeconds) } return enforcer, nil diff --git a/api/pkg/authz/enforcer/enforcer_test.go b/api/pkg/authz/enforcer/enforcer_test.go index 1059269f..5a0a351a 100644 --- a/api/pkg/authz/enforcer/enforcer_test.go +++ b/api/pkg/authz/enforcer/enforcer_test.go @@ -62,6 +62,58 @@ var BootstrapPolicy = []testPolicy{ }, } +func TestNewEnforcer(t *testing.T) { + tests := map[string]struct { + hostURL string + productName string + flavor Flavor + timeout time.Duration + cacheConfig *CacheConfig + + expectedError string + }{ + "success | no cache": { + hostURL: "http://authz.com", + productName: "mlp", + flavor: FlavorExact, + timeout: time.Second, + }, + "success | with cache": { + hostURL: "http://authz.com", + productName: "mlp", + flavor: FlavorExact, + timeout: time.Second, + cacheConfig: &CacheConfig{ + KeyExpirySeconds: 30, + CacheCleanUpIntervalSeconds: 60, + }, + }, + "failure | large cache expiry": { + hostURL: "http://authz.com", + productName: "mlp", + flavor: FlavorExact, + timeout: time.Second, + cacheConfig: &CacheConfig{ + KeyExpirySeconds: 3000, + CacheCleanUpIntervalSeconds: 60, + }, + expectedError: "Configured KeyExpirySeconds is larger than the max permitted value of 600", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + _, err := newEnforcer(tt.hostURL, tt.productName, tt.flavor, tt.timeout, tt.cacheConfig) + + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} + func TestEnforcer_Enforce(t *testing.T) { enforcer, err := NewEnforcerBuilder().URL(KetoURL).Product(ProductName).Build() assert.NoError(t, err) diff --git a/go.mod b/go.mod index 7a6a8741..58851832 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/newrelic/go-agent v3.19.2+incompatible github.com/opentracing/opentracing-go v1.1.0 github.com/ory/keto-client-go v0.4.4-alpha.1 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.11.1 github.com/prometheus/client_model v0.2.0 @@ -107,7 +108,6 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/common v0.26.0 // indirect diff --git a/go.sum b/go.sum index 515cf442..9984a013 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,10 @@ github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= @@ -173,6 +175,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= diff --git a/ui/packages/app/src/config.js b/ui/packages/app/src/config.js index 856a1d05..3ce499f5 100644 --- a/ui/packages/app/src/config.js +++ b/ui/packages/app/src/config.js @@ -19,12 +19,18 @@ const config = { "https://github.com/caraml-dev/merlin/blob/main/docs/getting-started/README.md", label: "Merlin User Guide" }, - { href: "https://github.com/caraml-dev/turing", label: "Turing User Guide" }, + { + href: "https://github.com/caraml-dev/turing", + label: "Turing User Guide" + }, { href: "https://docs.feast.dev/user-guide/overview", label: "Feast User Guide" } ], + MAX_AUTHZ_CACHE_EXPIRY_MINUTES: parseInt( + getEnv("REACT_APP_MAX_AUTHZ_CACHE_EXPIRY_MINUTES") || "0" + ), CLOCKWORK_UI_HOMEPAGE: getEnv("REACT_APP_CLOCKWORK_UI_HOMEPAGE"), KUBEFLOW_UI_HOMEPAGE: getEnv("REACT_APP_KUBEFLOW_UI_HOMEPAGE") diff --git a/ui/packages/app/src/project_setting/user_role/SubmitUserRoleForm.js b/ui/packages/app/src/project_setting/user_role/SubmitUserRoleForm.js index 34c5f3e6..bfab021b 100644 --- a/ui/packages/app/src/project_setting/user_role/SubmitUserRoleForm.js +++ b/ui/packages/app/src/project_setting/user_role/SubmitUserRoleForm.js @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -14,6 +15,7 @@ import { EuiTitle, EuiToolTip } from "@elastic/eui"; +import config from "../../config"; import { validateEmail } from "../../validation/validation"; import { addToast, useMlpApi } from "@caraml-dev/ui-lib"; import UserRoleSelection from "./UserRoleSelection"; @@ -77,6 +79,7 @@ const SubmitUserRoleForm = ({ userRole, project, fetchUpdates, toggleAdd }) => { onChange ]); + const isAuthzCacheEnabled = !!config.MAX_AUTHZ_CACHE_EXPIRY_MINUTES; return ( @@ -124,6 +127,21 @@ const SubmitUserRoleForm = ({ userRole, project, fetchUpdates, toggleAdd }) => { + + {isAuthzCacheEnabled && ( + 1 + ? "minutes" + : "minute" + } to take effect in all components.`} + iconType="iInCircle" + /> + )} +