зеркало из https://github.com/Azure/ARO-RP.git
Add IFxAudit Logging To Admin Portal (#1411)
* Add ifxaudit logging to admin portal Signed-off-by: Ivan Sim <isim@redhat.com> * Update comment in test Signed-off-by: Ivan Sim <isim@redhat.com>
This commit is contained in:
Родитель
f4aa7c1577
Коммит
5eb56f7ed3
|
@ -62,7 +62,7 @@ func main() {
|
|||
err = rp(ctx, log, audit)
|
||||
case "portal":
|
||||
checkArgs(1)
|
||||
err = portal(ctx, log)
|
||||
err = portal(ctx, log, audit)
|
||||
case "operator":
|
||||
checkArgs(2)
|
||||
err = operator(ctx, log)
|
||||
|
|
|
@ -24,7 +24,7 @@ import (
|
|||
"github.com/Azure/ARO-RP/pkg/util/keyvault"
|
||||
)
|
||||
|
||||
func portal(ctx context.Context, log *logrus.Entry) error {
|
||||
func portal(ctx context.Context, log *logrus.Entry, audit *logrus.Entry) error {
|
||||
_env, err := env.NewCore(ctx, log)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -166,7 +166,7 @@ func portal(ctx context.Context, log *logrus.Entry) error {
|
|||
|
||||
log.Print("listening")
|
||||
|
||||
p := pkgportal.NewPortal(_env, log.WithField("component", "portal"), log.WithField("component", "portal-access"), l, sshl, verifier, hostname, servingKey, servingCerts, clientID, clientKey, clientCerts, sessionKey, sshKey, groupIDs, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, dialer)
|
||||
p := pkgportal.NewPortal(_env, audit, log.WithField("component", "portal"), log.WithField("component", "portal-access"), l, sshl, verifier, hostname, servingKey, servingCerts, clientID, clientKey, clientCerts, sessionKey, sshKey, groupIDs, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, dialer)
|
||||
|
||||
return p.Run(ctx)
|
||||
}
|
||||
|
|
|
@ -495,6 +495,8 @@ func TestSecurity(t *testing.T) {
|
|||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer auditHook.Reset()
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: pool,
|
||||
}
|
||||
|
@ -527,7 +529,6 @@ func TestSecurity(t *testing.T) {
|
|||
}
|
||||
|
||||
testlog.AssertAuditPayloads(t, auditHook, tt.wantAuditPayloads)
|
||||
auditHook.Entries = []logrus.Entry{}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/api/validate"
|
||||
"github.com/Azure/ARO-RP/pkg/database"
|
||||
"github.com/Azure/ARO-RP/pkg/env"
|
||||
"github.com/Azure/ARO-RP/pkg/portal/middleware"
|
||||
"github.com/Azure/ARO-RP/pkg/portal/util/clientcache"
|
||||
"github.com/Azure/ARO-RP/pkg/proxy"
|
||||
|
@ -49,6 +50,8 @@ type kubeconfig struct {
|
|||
}
|
||||
|
||||
func New(baseLog *logrus.Entry,
|
||||
audit *logrus.Entry,
|
||||
env env.Core,
|
||||
baseAccessLog *logrus.Entry,
|
||||
servingCert *x509.Certificate,
|
||||
elevatedGroupIDs []string,
|
||||
|
@ -83,7 +86,7 @@ func New(baseLog *logrus.Entry,
|
|||
|
||||
bearerAuthenticatedRouter := unauthenticatedRouter.NewRoute().Subrouter()
|
||||
bearerAuthenticatedRouter.Use(middleware.Bearer(k.dbPortal))
|
||||
bearerAuthenticatedRouter.Use(middleware.Log(k.baseAccessLog))
|
||||
bearerAuthenticatedRouter.Use(middleware.Log(env, audit, k.baseAccessLog))
|
||||
|
||||
bearerAuthenticatedRouter.PathPrefix("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/kubeconfig/proxy/").Handler(rp)
|
||||
|
||||
|
|
|
@ -12,15 +12,17 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
|
||||
"github.com/Azure/ARO-RP/pkg/portal/middleware"
|
||||
"github.com/Azure/ARO-RP/pkg/portal/util/responsewriter"
|
||||
mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
|
||||
testdatabase "github.com/Azure/ARO-RP/test/database"
|
||||
testlog "github.com/Azure/ARO-RP/test/util/log"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
|
@ -136,9 +138,17 @@ func TestNew(t *testing.T) {
|
|||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
_env := mock_env.NewMockInterface(ctrl)
|
||||
_env.EXPECT().Environment().AnyTimes().Return(&azure.PublicCloud)
|
||||
_env.EXPECT().Hostname().AnyTimes().Return("testhost")
|
||||
_env.EXPECT().Location().AnyTimes().Return("eastus")
|
||||
|
||||
aadAuthenticatedRouter := &mux.Router{}
|
||||
|
||||
k := New(logrus.NewEntry(logrus.StandardLogger()), nil, servingCert, elevatedGroupIDs, nil, dbPortal, nil, aadAuthenticatedRouter, &mux.Router{})
|
||||
_, audit := testlog.NewAudit()
|
||||
_, baseLog := testlog.New()
|
||||
_, baseAccessLog := testlog.New()
|
||||
k := New(baseLog, audit, _env, baseAccessLog, servingCert, elevatedGroupIDs, nil, dbPortal, nil, aadAuthenticatedRouter, &mux.Router{})
|
||||
|
||||
k.newToken = func() string { return password }
|
||||
|
||||
|
|
|
@ -15,18 +15,20 @@ import (
|
|||
"net/http/httputil"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sirupsen/logrus"
|
||||
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
|
||||
"github.com/Azure/ARO-RP/pkg/portal/util/responsewriter"
|
||||
mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
|
||||
mock_proxy "github.com/Azure/ARO-RP/pkg/util/mocks/proxy"
|
||||
utiltls "github.com/Azure/ARO-RP/pkg/util/tls"
|
||||
testdatabase "github.com/Azure/ARO-RP/test/database"
|
||||
"github.com/Azure/ARO-RP/test/util/listener"
|
||||
testlog "github.com/Azure/ARO-RP/test/util/log"
|
||||
)
|
||||
|
||||
// fakeServer returns a test listener for an HTTPS server which validates its
|
||||
|
@ -381,6 +383,11 @@ func TestProxy(t *testing.T) {
|
|||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
_env := mock_env.NewMockInterface(ctrl)
|
||||
_env.EXPECT().Environment().AnyTimes().Return(&azure.PublicCloud)
|
||||
_env.EXPECT().Hostname().AnyTimes().Return("testhost")
|
||||
_env.EXPECT().Location().AnyTimes().Return("eastus")
|
||||
|
||||
dialer := mock_proxy.NewMockDialer(ctrl)
|
||||
if tt.mocks != nil {
|
||||
tt.mocks(dialer)
|
||||
|
@ -388,7 +395,10 @@ func TestProxy(t *testing.T) {
|
|||
|
||||
unauthenticatedRouter := &mux.Router{}
|
||||
|
||||
k := New(logrus.NewEntry(logrus.StandardLogger()), logrus.NewEntry(logrus.StandardLogger()), nil, nil, dbOpenShiftClusters, dbPortal, dialer, &mux.Router{}, unauthenticatedRouter)
|
||||
_, audit := testlog.NewAudit()
|
||||
_, baseLog := testlog.New()
|
||||
_, baseAccessLog := testlog.New()
|
||||
k := New(baseLog, audit, _env, baseAccessLog, nil, nil, dbOpenShiftClusters, dbPortal, dialer, &mux.Router{}, unauthenticatedRouter)
|
||||
|
||||
k.newToken = func() string { return token }
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ type aad struct {
|
|||
}
|
||||
|
||||
func NewAAD(log *logrus.Entry,
|
||||
audit *logrus.Entry,
|
||||
env env.Core,
|
||||
baseAccessLog *logrus.Entry,
|
||||
hostname string,
|
||||
|
@ -151,7 +152,7 @@ func NewAAD(log *logrus.Entry,
|
|||
a.store.Options.HttpOnly = true
|
||||
a.store.Options.SameSite = http.SameSiteLaxMode
|
||||
|
||||
unauthenticatedRouter.NewRoute().Methods(http.MethodGet).Path("/callback").Handler(Log(baseAccessLog)(http.HandlerFunc(a.callback)))
|
||||
unauthenticatedRouter.NewRoute().Methods(http.MethodGet).Path("/callback").Handler(Log(env, audit, baseAccessLog)(http.HandlerFunc(a.callback)))
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
|
|
@ -23,12 +23,12 @@ import (
|
|||
"github.com/golang/mock/gomock"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
|
||||
"github.com/Azure/ARO-RP/pkg/util/roundtripper"
|
||||
utiltls "github.com/Azure/ARO-RP/pkg/util/tls"
|
||||
testlog "github.com/Azure/ARO-RP/test/util/log"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -81,7 +81,7 @@ func (c noopClaims) Claims(v interface{}) error {
|
|||
}
|
||||
|
||||
func TestNewAAD(t *testing.T) {
|
||||
_, err := NewAAD(nil, nil, nil, "", nil, "", nil, nil, nil, nil, nil)
|
||||
_, err := NewAAD(nil, nil, nil, nil, "", nil, "", nil, nil, nil, nil, nil)
|
||||
if err.Error() != "invalid sessionKey" {
|
||||
t.Error(err)
|
||||
}
|
||||
|
@ -162,7 +162,10 @@ func TestAAD(t *testing.T) {
|
|||
env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
|
||||
env.EXPECT().TenantID().AnyTimes().Return("")
|
||||
|
||||
a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
|
||||
_, audit := testlog.NewAudit()
|
||||
_, baseLog := testlog.New()
|
||||
_, baseAccessLog := testlog.New()
|
||||
a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -258,7 +261,10 @@ func TestRedirect(t *testing.T) {
|
|||
env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
|
||||
env.EXPECT().TenantID().AnyTimes().Return("")
|
||||
|
||||
a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
|
||||
_, audit := testlog.NewAudit()
|
||||
_, baseLog := testlog.New()
|
||||
_, baseAccessLog := testlog.New()
|
||||
a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -374,7 +380,10 @@ func TestLogout(t *testing.T) {
|
|||
env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
|
||||
env.EXPECT().TenantID().AnyTimes().Return("")
|
||||
|
||||
a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
|
||||
_, audit := testlog.NewAudit()
|
||||
_, baseLog := testlog.New()
|
||||
_, baseAccessLog := testlog.New()
|
||||
a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -736,7 +745,10 @@ func TestCallback(t *testing.T) {
|
|||
env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
|
||||
env.EXPECT().TenantID().AnyTimes().Return("")
|
||||
|
||||
a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), clientID, clientkey, clientcerts, groups, mux.NewRouter(), tt.verifier)
|
||||
_, audit := testlog.NewAudit()
|
||||
_, baseLog := testlog.New()
|
||||
_, baseAccessLog := testlog.New()
|
||||
a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), clientID, clientkey, clientcerts, groups, mux.NewRouter(), tt.verifier)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -854,8 +866,10 @@ func TestClientAssertion(t *testing.T) {
|
|||
env.EXPECT().TenantID().AnyTimes().Return("")
|
||||
|
||||
clientID := "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), clientID, clientkey, clientcerts, nil, mux.NewRouter(), nil)
|
||||
_, audit := testlog.NewAudit()
|
||||
_, baseLog := testlog.New()
|
||||
_, baseAccessLog := testlog.New()
|
||||
a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), clientID, clientkey, clientcerts, nil, mux.NewRouter(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ package middleware
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
@ -12,7 +13,9 @@ import (
|
|||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/env"
|
||||
utillog "github.com/Azure/ARO-RP/pkg/util/log"
|
||||
"github.com/Azure/ARO-RP/pkg/util/log/audit"
|
||||
)
|
||||
|
||||
type logResponseWriter struct {
|
||||
|
@ -50,7 +53,7 @@ func (rc *logReadCloser) Read(b []byte) (int, error) {
|
|||
return n, err
|
||||
}
|
||||
|
||||
func Log(baseLog *logrus.Entry) func(http.Handler) http.Handler {
|
||||
func Log(env env.Core, auditLog, baseLog *logrus.Entry) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t := time.Now()
|
||||
|
@ -73,16 +76,64 @@ func Log(baseLog *logrus.Entry) func(http.Handler) http.Handler {
|
|||
})
|
||||
log.Print("read request")
|
||||
|
||||
auditEntry := auditLog.WithFields(logrus.Fields{
|
||||
audit.MetadataAdminOperation: true,
|
||||
audit.MetadataCreatedTime: time.Now().UTC().Format(time.RFC3339),
|
||||
audit.MetadataLogKind: audit.IFXAuditLogKind,
|
||||
audit.MetadataSource: audit.SourceAdminPortal,
|
||||
audit.EnvKeyAppID: audit.SourceAdminPortal,
|
||||
audit.EnvKeyCloudRole: audit.CloudRoleRP,
|
||||
audit.EnvKeyEnvironment: env.Environment().Name,
|
||||
audit.EnvKeyHostname: env.Hostname(),
|
||||
audit.EnvKeyLocation: env.Location(),
|
||||
audit.PayloadKeyCategory: audit.CategoryResourceManagement,
|
||||
audit.PayloadKeyOperationName: fmt.Sprintf("%s %s", r.Method, r.URL.Path),
|
||||
audit.PayloadKeyCallerIdentities: []audit.CallerIdentity{
|
||||
{
|
||||
CallerIdentityType: audit.CallerIdentityTypeUsername,
|
||||
CallerIdentityValue: username,
|
||||
CallerIPAddress: r.RemoteAddr,
|
||||
},
|
||||
},
|
||||
audit.PayloadKeyTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceName: r.URL.Path,
|
||||
TargetResourceType: auditTargetResourceType(r),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
defer func() {
|
||||
statusCode := w.(*logResponseWriter).statusCode
|
||||
log.WithFields(logrus.Fields{
|
||||
"body_read_bytes": r.Body.(*logReadCloser).bytes,
|
||||
"body_written_bytes": w.(*logResponseWriter).bytes,
|
||||
"duration": time.Since(t).Seconds(),
|
||||
"response_status_code": w.(*logResponseWriter).statusCode,
|
||||
"response_status_code": statusCode,
|
||||
}).Print("sent response")
|
||||
|
||||
resultType := audit.ResultTypeSuccess
|
||||
if statusCode >= http.StatusBadRequest {
|
||||
resultType = audit.ResultTypeFail
|
||||
}
|
||||
|
||||
auditEntry.WithFields(logrus.Fields{
|
||||
audit.PayloadKeyResult: audit.Result{
|
||||
ResultType: resultType,
|
||||
ResultDescription: fmt.Sprintf("Status code: %d", statusCode),
|
||||
},
|
||||
}).Info(audit.DefaultLogMessage)
|
||||
}()
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func auditTargetResourceType(r *http.Request) string {
|
||||
if matches := utillog.RXTolerantSubResourceID.FindStringSubmatch(r.URL.Path); matches != nil {
|
||||
return matches[len(matches)-1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -9,18 +9,31 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/util/log/audit"
|
||||
mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
|
||||
testlog "github.com/Azure/ARO-RP/test/util/log"
|
||||
)
|
||||
|
||||
func TestLog(t *testing.T) {
|
||||
h, log := testlog.New()
|
||||
ah, auditLog := testlog.NewAudit()
|
||||
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
_env := mock_env.NewMockInterface(controller)
|
||||
_env.EXPECT().Environment().AnyTimes().Return(&azure.PublicCloud)
|
||||
_env.EXPECT().Hostname().AnyTimes().Return("testhost")
|
||||
_env.EXPECT().Location().AnyTimes().Return("eastus")
|
||||
|
||||
ctx := context.WithValue(context.Background(), ContextKeyUsername, "username")
|
||||
r, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://localhost/", strings.NewReader("body"))
|
||||
|
@ -32,7 +45,8 @@ func TestLog(t *testing.T) {
|
|||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
Log(log)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// chain a custom handler with the Log middleware to mutate the request
|
||||
Log(_env, auditLog, log)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL = nil // mutate the request
|
||||
|
||||
_ = w.(http.Hijacker) // must implement http.Hijacker
|
||||
|
@ -76,7 +90,115 @@ func TestLog(t *testing.T) {
|
|||
t.Error(err)
|
||||
}
|
||||
|
||||
// only one audit log is associated with the one POST request
|
||||
expectedAudit := []*audit.Payload{
|
||||
{
|
||||
EnvVer: audit.IFXAuditVersion,
|
||||
EnvName: audit.IFXAuditName,
|
||||
EnvFlags: 257,
|
||||
EnvAppID: audit.SourceAdminPortal,
|
||||
EnvCloudName: _env.Environment().Name,
|
||||
EnvCloudRole: audit.CloudRoleRP,
|
||||
EnvCloudRoleInstance: _env.Hostname(),
|
||||
EnvCloudEnvironment: _env.Environment().Name,
|
||||
EnvCloudLocation: _env.Location(),
|
||||
EnvCloudVer: audit.IFXAuditCloudVer,
|
||||
CallerIdentities: []audit.CallerIdentity{
|
||||
{
|
||||
CallerIdentityType: audit.CallerIdentityTypeUsername,
|
||||
CallerIdentityValue: "username",
|
||||
CallerIPAddress: "127.0.0.1:1234",
|
||||
},
|
||||
},
|
||||
Category: "ResourceManagement",
|
||||
OperationName: "POST /",
|
||||
Result: audit.Result{
|
||||
ResultType: "Success",
|
||||
ResultDescription: "Status code: 200",
|
||||
},
|
||||
TargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "",
|
||||
TargetResourceName: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
testlog.AssertAuditPayloads(t, ah, expectedAudit)
|
||||
|
||||
for _, e := range h.Entries {
|
||||
fmt.Println(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAuditTargetResourceData(t *testing.T) {
|
||||
var (
|
||||
testSubscription = "test-sub"
|
||||
testResourceGroup = "test-rg"
|
||||
testResource = "test-resource"
|
||||
)
|
||||
|
||||
var testCases = []struct {
|
||||
url string
|
||||
expectedType string
|
||||
expectedName string
|
||||
}{
|
||||
{
|
||||
url: "/",
|
||||
expectedName: "/",
|
||||
},
|
||||
{
|
||||
url: "/index.html",
|
||||
expectedName: "/index.html",
|
||||
},
|
||||
{
|
||||
url: "/lib/bootstrap-4.5.2.min.css",
|
||||
expectedName: "/lib/bootstrap-4.5.2.min.css",
|
||||
},
|
||||
{
|
||||
url: "/api/clusters",
|
||||
expectedName: "/api/clusters",
|
||||
},
|
||||
{
|
||||
url: "/api/logout",
|
||||
expectedName: "/api/logout",
|
||||
},
|
||||
{
|
||||
url: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/ssh/new", testSubscription, testResourceGroup, testResource),
|
||||
expectedName: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/ssh/new", testSubscription, testResourceGroup, testResource),
|
||||
expectedType: "ssh",
|
||||
},
|
||||
{
|
||||
url: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/kubeconfig/new", testSubscription, testResourceGroup, testResource),
|
||||
expectedName: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/kubeconfig/new", testSubscription, testResourceGroup, testResource),
|
||||
expectedType: "kubeconfig",
|
||||
},
|
||||
{
|
||||
url: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/kubeconfig/proxy", testSubscription, testResourceGroup, testResource),
|
||||
expectedName: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/kubeconfig/proxy", testSubscription, testResourceGroup, testResource),
|
||||
expectedType: "kubeconfig",
|
||||
},
|
||||
{
|
||||
url: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/prometheus", testSubscription, testResourceGroup, testResource),
|
||||
expectedName: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/prometheus", testSubscription, testResourceGroup, testResource),
|
||||
expectedType: "prometheus",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
parsedURL, err := url.Parse(tc.url)
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error: ", err)
|
||||
}
|
||||
|
||||
r := &http.Request{URL: parsedURL}
|
||||
if actual := auditTargetResourceType(r); tc.expectedType != actual {
|
||||
t.Errorf("%s: expected: %s, actual: %s", tc.url, tc.expectedType, actual)
|
||||
}
|
||||
|
||||
if actual := r.URL.Path; tc.expectedName != actual {
|
||||
t.Errorf("%s: expected: %s, actual: %s", tc.url, tc.expectedName, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ type Runnable interface {
|
|||
|
||||
type portal struct {
|
||||
env env.Core
|
||||
audit *logrus.Entry
|
||||
log *logrus.Entry
|
||||
baseAccessLog *logrus.Entry
|
||||
l net.Listener
|
||||
|
@ -67,6 +68,7 @@ type portal struct {
|
|||
}
|
||||
|
||||
func NewPortal(env env.Core,
|
||||
audit *logrus.Entry,
|
||||
log *logrus.Entry,
|
||||
baseAccessLog *logrus.Entry,
|
||||
l net.Listener,
|
||||
|
@ -87,6 +89,7 @@ func NewPortal(env env.Core,
|
|||
dialer proxy.Dialer) Runnable {
|
||||
return &portal{
|
||||
env: env,
|
||||
audit: audit,
|
||||
log: log,
|
||||
baseAccessLog: baseAccessLog,
|
||||
l: l,
|
||||
|
@ -160,14 +163,14 @@ func (p *portal) Run(ctx context.Context) error {
|
|||
allGroups := append([]string{}, p.groupIDs...)
|
||||
allGroups = append(allGroups, p.elevatedGroupIDs...)
|
||||
|
||||
p.aad, err = middleware.NewAAD(p.log, p.env, p.baseAccessLog, p.hostname, p.sessionKey, p.clientID, p.clientKey, p.clientCerts, allGroups, unauthenticatedRouter, p.verifier)
|
||||
p.aad, err = middleware.NewAAD(p.log, p.audit, p.env, p.baseAccessLog, p.hostname, p.sessionKey, p.clientID, p.clientKey, p.clientCerts, allGroups, unauthenticatedRouter, p.verifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aadAuthenticatedRouter := r.NewRoute().Subrouter()
|
||||
aadAuthenticatedRouter.Use(p.aad.AAD)
|
||||
aadAuthenticatedRouter.Use(middleware.Log(p.baseAccessLog))
|
||||
aadAuthenticatedRouter.Use(middleware.Log(p.env, p.audit, p.baseAccessLog))
|
||||
aadAuthenticatedRouter.Use(p.aad.Redirect)
|
||||
aadAuthenticatedRouter.Use(csrf.Protect(p.sessionKey, csrf.SameSite(csrf.SameSiteStrictMode), csrf.MaxAge(0)))
|
||||
|
||||
|
@ -183,7 +186,7 @@ func (p *portal) Run(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
kubeconfig.New(p.log, p.baseAccessLog, p.servingCerts[0], p.elevatedGroupIDs, p.dbOpenShiftClusters, p.dbPortal, p.dialer, aadAuthenticatedRouter, unauthenticatedRouter)
|
||||
kubeconfig.New(p.log, p.audit, p.env, p.baseAccessLog, p.servingCerts[0], p.elevatedGroupIDs, p.dbOpenShiftClusters, p.dbPortal, p.dialer, aadAuthenticatedRouter, unauthenticatedRouter)
|
||||
|
||||
prometheus.New(p.log, p.dbOpenShiftClusters, p.dialer, aadAuthenticatedRouter)
|
||||
|
||||
|
@ -199,7 +202,7 @@ func (p *portal) Run(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (p *portal) unauthenticatedRoutes(r *mux.Router) {
|
||||
logger := middleware.Log(p.baseAccessLog)
|
||||
logger := middleware.Log(p.env, p.audit, p.baseAccessLog)
|
||||
|
||||
r.NewRoute().Methods(http.MethodGet).Path("/healthz/ready").Handler(logger(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})))
|
||||
}
|
||||
|
|
|
@ -9,21 +9,25 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/portal/middleware"
|
||||
"github.com/Azure/ARO-RP/pkg/util/log/audit"
|
||||
mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
|
||||
utiltls "github.com/Azure/ARO-RP/pkg/util/tls"
|
||||
testdatabase "github.com/Azure/ARO-RP/test/database"
|
||||
"github.com/Azure/ARO-RP/test/util/listener"
|
||||
testlog "github.com/Azure/ARO-RP/test/util/log"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -34,6 +38,10 @@ func TestSecurity(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
log := logrus.NewEntry(logrus.StandardLogger())
|
||||
|
||||
_, portalAccessLog := testlog.New()
|
||||
_, portalLog := testlog.New()
|
||||
auditHook, portalAuditLog := testlog.NewAudit()
|
||||
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
|
@ -41,6 +49,8 @@ func TestSecurity(t *testing.T) {
|
|||
_env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
|
||||
_env.EXPECT().Location().AnyTimes().Return("eastus")
|
||||
_env.EXPECT().TenantID().AnyTimes().Return("00000000-0000-0000-0000-000000000001")
|
||||
_env.EXPECT().Environment().AnyTimes().Return(&azure.PublicCloud)
|
||||
_env.EXPECT().Hostname().AnyTimes().Return("testhost")
|
||||
|
||||
l := listener.NewListener()
|
||||
defer l.Close()
|
||||
|
@ -76,7 +86,7 @@ func TestSecurity(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
p := NewPortal(_env, log, log, l, sshl, nil, "", serverkey, servercerts, "", nil, nil, make([]byte, 32), sshkey, nil, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, nil)
|
||||
p := NewPortal(_env, portalAuditLog, portalLog, portalAccessLog, l, sshl, nil, "", serverkey, servercerts, "", nil, nil, make([]byte, 32), sshkey, nil, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, nil)
|
||||
go func() {
|
||||
err := p.Run(ctx)
|
||||
if err != nil {
|
||||
|
@ -90,24 +100,47 @@ func TestSecurity(t *testing.T) {
|
|||
checkResponse func(*testing.T, bool, bool, *http.Response)
|
||||
unauthenticatedWantStatusCode int
|
||||
authenticatedWantStatusCode int
|
||||
wantAuditOperation string
|
||||
wantAuditTargetResources []audit.TargetResource
|
||||
}{
|
||||
{
|
||||
name: "/",
|
||||
request: func() (*http.Request, error) {
|
||||
return http.NewRequest(http.MethodGet, "https://server/", nil)
|
||||
},
|
||||
wantAuditOperation: "GET /",
|
||||
wantAuditTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "",
|
||||
TargetResourceName: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/index.js",
|
||||
request: func() (*http.Request, error) {
|
||||
return http.NewRequest(http.MethodGet, "https://server/index.js", nil)
|
||||
},
|
||||
wantAuditOperation: "GET /index.js",
|
||||
wantAuditTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "",
|
||||
TargetResourceName: "/index.js",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/api/clusters",
|
||||
request: func() (*http.Request, error) {
|
||||
return http.NewRequest(http.MethodGet, "https://server/api/clusters", nil)
|
||||
},
|
||||
wantAuditOperation: "GET /api/clusters",
|
||||
wantAuditTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "",
|
||||
TargetResourceName: "/api/clusters",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/api/logout",
|
||||
|
@ -115,6 +148,13 @@ func TestSecurity(t *testing.T) {
|
|||
return http.NewRequest(http.MethodPost, "https://server/api/logout", nil)
|
||||
},
|
||||
authenticatedWantStatusCode: http.StatusSeeOther,
|
||||
wantAuditOperation: "POST /api/logout",
|
||||
wantAuditTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "",
|
||||
TargetResourceName: "/api/logout",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/callback",
|
||||
|
@ -122,6 +162,13 @@ func TestSecurity(t *testing.T) {
|
|||
return http.NewRequest(http.MethodGet, "https://server/callback", nil)
|
||||
},
|
||||
authenticatedWantStatusCode: http.StatusTemporaryRedirect,
|
||||
wantAuditOperation: "GET /callback",
|
||||
wantAuditTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "",
|
||||
TargetResourceName: "/callback",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/healthz/ready",
|
||||
|
@ -129,12 +176,26 @@ func TestSecurity(t *testing.T) {
|
|||
return http.NewRequest(http.MethodGet, "https://server/healthz/ready", nil)
|
||||
},
|
||||
unauthenticatedWantStatusCode: http.StatusOK,
|
||||
wantAuditOperation: "GET /healthz/ready",
|
||||
wantAuditTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "",
|
||||
TargetResourceName: "/healthz/ready",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/kubeconfig/new",
|
||||
request: func() (*http.Request, error) {
|
||||
return http.NewRequest(http.MethodPost, "https://server/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourceGroupName/providers/microsoft.redhatopenshift/openshiftclusters/resourceName/kubeconfig/new", nil)
|
||||
},
|
||||
wantAuditOperation: "POST /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/kubeconfig/new",
|
||||
wantAuditTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "kubeconfig",
|
||||
TargetResourceName: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/kubeconfig/new",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/prometheus",
|
||||
|
@ -142,6 +203,13 @@ func TestSecurity(t *testing.T) {
|
|||
return http.NewRequest(http.MethodPost, "https://server/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourceGroupName/providers/microsoft.redhatopenshift/openshiftclusters/resourceName/prometheus", nil)
|
||||
},
|
||||
authenticatedWantStatusCode: http.StatusTemporaryRedirect,
|
||||
wantAuditOperation: "POST /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/prometheus",
|
||||
wantAuditTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "prometheus",
|
||||
TargetResourceName: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/prometheus",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/ssh/new",
|
||||
|
@ -168,6 +236,13 @@ func TestSecurity(t *testing.T) {
|
|||
}
|
||||
}
|
||||
},
|
||||
wantAuditOperation: "POST /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/ssh/new",
|
||||
wantAuditTargetResources: []audit.TargetResource{
|
||||
{
|
||||
TargetResourceType: "ssh",
|
||||
TargetResourceName: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/ssh/new",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/doesnotexist",
|
||||
|
@ -201,6 +276,8 @@ func TestSecurity(t *testing.T) {
|
|||
},
|
||||
} {
|
||||
t.Run(tt2.name+tt.name, func(t *testing.T) {
|
||||
defer auditHook.Reset()
|
||||
|
||||
req, err := tt.request()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -243,6 +320,36 @@ func TestSecurity(t *testing.T) {
|
|||
if tt.checkResponse != nil {
|
||||
tt.checkResponse(t, tt2.authenticated, tt2.elevated, resp)
|
||||
}
|
||||
|
||||
// no audit logs for https://server/doesnotexist
|
||||
if tt.authenticatedWantStatusCode == http.StatusNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
// skipping https://server because the http.ServeContent() calls in the
|
||||
// portal's serve() and index() handlers[1] issued a call to io.Copy()[2]
|
||||
// causes a race condition with the audit hook. The response was returned
|
||||
// to the client and the testlog.AssertAuditPayloads() was called immediately,
|
||||
// while the audit hook was still in-flight.
|
||||
//
|
||||
// note that the audit logs will still be recorded and emitted by the audit
|
||||
// hook, so this is a non-issue in the Geneva environment.
|
||||
//
|
||||
// [1] https://github.com/Azure/ARO-RP/blob/master/pkg/portal/portal.go#L222-L247
|
||||
// [2] https://go.googlesource.com/go/+/go1.16.2/src/net/http/fs.go#337
|
||||
if tt.name == "/" || tt.name == "/index.js" {
|
||||
return
|
||||
}
|
||||
|
||||
payload := auditPayloadFixture()
|
||||
payload.OperationName = tt.wantAuditOperation
|
||||
payload.TargetResources = tt.wantAuditTargetResources
|
||||
payload.Result.ResultDescription = fmt.Sprintf("Status code: %d", tt2.wantStatusCode)
|
||||
if tt2.authenticated && tt.name != "/callback" && tt.name != "/healthz/ready" {
|
||||
payload.CallerIdentities[0].CallerIdentityValue = "username"
|
||||
}
|
||||
|
||||
testlog.AssertAuditPayloads(t, auditHook, []*audit.Payload{payload})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -282,3 +389,29 @@ func addAuth(req *http.Request, groups []string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func auditPayloadFixture() *audit.Payload {
|
||||
return &audit.Payload{
|
||||
EnvVer: audit.IFXAuditVersion,
|
||||
EnvName: audit.IFXAuditName,
|
||||
EnvFlags: 257,
|
||||
EnvAppID: audit.SourceAdminPortal,
|
||||
EnvCloudName: azure.PublicCloud.Name,
|
||||
EnvCloudRole: audit.CloudRoleRP,
|
||||
EnvCloudRoleInstance: "testhost",
|
||||
EnvCloudEnvironment: azure.PublicCloud.Name,
|
||||
EnvCloudLocation: "eastus",
|
||||
EnvCloudVer: 1,
|
||||
CallerIdentities: []audit.CallerIdentity{
|
||||
{
|
||||
CallerDisplayName: "",
|
||||
CallerIdentityType: audit.CallerIdentityTypeUsername,
|
||||
CallerIPAddress: "bufferedpipe",
|
||||
},
|
||||
},
|
||||
Category: audit.CategoryResourceManagement,
|
||||
Result: audit.Result{
|
||||
ResultType: audit.ResultTypeSuccess,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ var (
|
|||
RXAdminProvider = regexp.MustCompile(`^/admin/providers/([^/]+)/([^/]+)$`)
|
||||
|
||||
RXTolerantResourceID = regexp.MustCompile(`(?i)^(?:/admin)?/subscriptions/([^/]+)(?:/resourceGroups/([^/]+)(?:/providers/([^/]+)/([^/]+)(?:/([^/]+))?)?)?`)
|
||||
|
||||
RXTolerantSubResourceID = regexp.MustCompile(`(?i)^(?:/admin)?/subscriptions/([^/]+)(?:/resourceGroups/([^/]+)(?:/providers/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+))?)?)?`)
|
||||
)
|
||||
|
||||
func getBaseLogger() *logrus.Logger {
|
||||
|
|
Загрузка…
Ссылка в новой задаче