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:
Ivan Sim 2021-04-20 02:01:06 -07:00 коммит произвёл GitHub
Родитель f4aa7c1577
Коммит 5eb56f7ed3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 376 добавлений и 26 удалений

Просмотреть файл

@ -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 {