zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/log/log_test.go (about)

     1  //go:build sync && scrub && metrics && search
     2  // +build sync,scrub,metrics,search
     3  
     4  package log_test
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"os"
    13  	"path"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	godigest "github.com/opencontainers/go-digest"
    19  	"github.com/rs/zerolog"
    20  	. "github.com/smartystreets/goconvey/convey"
    21  	"gopkg.in/resty.v1"
    22  
    23  	"zotregistry.dev/zot/pkg/api"
    24  	"zotregistry.dev/zot/pkg/api/config"
    25  	"zotregistry.dev/zot/pkg/api/constants"
    26  	"zotregistry.dev/zot/pkg/log"
    27  	test "zotregistry.dev/zot/pkg/test/common"
    28  )
    29  
    30  type AuditLog struct {
    31  	Level    string `json:"level"`
    32  	ClientIP string `json:"clientIP"` //nolint:tagliatelle // keep IP
    33  	Subject  string `json:"subject"`
    34  	Action   string `json:"action"`
    35  	Object   string `json:"object"`
    36  	Status   int    `json:"status"`
    37  	Time     string `json:"time"`
    38  	Message  string `json:"message"`
    39  }
    40  
    41  func TestAuditLogMessages(t *testing.T) {
    42  	Convey("Make a new controller", t, func() {
    43  		dir := t.TempDir()
    44  
    45  		port := test.GetFreePort()
    46  		baseURL := test.GetBaseURL(port)
    47  		conf := config.New()
    48  
    49  		outputPath := dir + "/zot.log"
    50  		auditPath := dir + "/zot-audit.log"
    51  		conf.Log = &config.LogConfig{Level: "debug", Output: outputPath, Audit: auditPath}
    52  
    53  		conf.HTTP.Port = port
    54  
    55  		username, seedUser := test.GenerateRandomString()
    56  		password, seedPass := test.GenerateRandomString()
    57  		htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
    58  		defer os.Remove(htpasswdPath)
    59  		conf.HTTP.Auth = &config.AuthConfig{
    60  			HTPasswd: config.AuthHTPasswd{
    61  				Path: htpasswdPath,
    62  			},
    63  		}
    64  
    65  		ctlr := api.NewController(conf)
    66  		ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
    67  		ctlr.Config.Storage.RootDirectory = dir
    68  
    69  		ctlrManager := test.NewControllerManager(ctlr)
    70  		ctlrManager.StartAndWait(port)
    71  		defer ctlrManager.StopServer()
    72  
    73  		Convey("Open auditLog file", func() {
    74  			auditFile, err := os.Open(auditPath)
    75  			if err != nil {
    76  				t.Log("Cannot open file")
    77  				panic(err)
    78  			}
    79  			defer auditFile.Close()
    80  
    81  			Convey("Test GET request", func() {
    82  				resp, err := resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
    83  				So(err, ShouldBeNil)
    84  				So(resp, ShouldNotBeNil)
    85  				So(resp.StatusCode(), ShouldEqual, http.StatusOK)
    86  
    87  				byteValue, _ := io.ReadAll(auditFile)
    88  				So(len(byteValue), ShouldEqual, 0)
    89  			})
    90  
    91  			Convey("Test POST request", func() {
    92  				repoName := "everyone/isallowed"
    93  				path := "/v2/" + repoName + "/blobs/uploads/"
    94  				resp, err := resty.R().SetBasicAuth(username, password).Post(baseURL + path)
    95  				So(err, ShouldBeNil)
    96  				So(resp, ShouldNotBeNil)
    97  				So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
    98  
    99  				// wait until the file is populated
   100  				byteValue, _ := io.ReadAll(auditFile)
   101  				for {
   102  					if len(byteValue) != 0 {
   103  						break
   104  					}
   105  					time.Sleep(100 * time.Millisecond)
   106  					byteValue, _ = io.ReadAll(auditFile)
   107  				}
   108  
   109  				var auditLog AuditLog
   110  				err = json.Unmarshal(byteValue, &auditLog)
   111  				if err != nil {
   112  					panic(err)
   113  				}
   114  
   115  				So(auditLog.Subject, ShouldEqual, username)
   116  				So(auditLog.Action, ShouldEqual, http.MethodPost)
   117  				So(auditLog.Status, ShouldEqual, http.StatusAccepted)
   118  				So(auditLog.Object, ShouldEqual, path)
   119  			})
   120  
   121  			Convey("Test PUT and DELETE request", func() {
   122  				// create upload
   123  				path := "/v2/repo/blobs/uploads/"
   124  				resp, err := resty.R().SetBasicAuth(username, password).Post(baseURL + path)
   125  				So(err, ShouldBeNil)
   126  				So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
   127  				loc := test.Location(baseURL, resp)
   128  				So(loc, ShouldNotBeEmpty)
   129  				location := resp.Header().Get("Location")
   130  				So(location, ShouldNotBeEmpty)
   131  
   132  				// wait until the file is populated
   133  				byteValue, _ := io.ReadAll(auditFile)
   134  				for {
   135  					if len(byteValue) != 0 {
   136  						break
   137  					}
   138  					time.Sleep(100 * time.Millisecond)
   139  					byteValue, _ = io.ReadAll(auditFile)
   140  				}
   141  
   142  				var auditLog AuditLog
   143  				err = json.Unmarshal(byteValue, &auditLog)
   144  				if err != nil {
   145  					panic(err)
   146  				}
   147  
   148  				So(auditLog.Subject, ShouldEqual, username)
   149  				So(auditLog.Action, ShouldEqual, http.MethodPost)
   150  				So(auditLog.Status, ShouldEqual, http.StatusAccepted)
   151  				So(auditLog.Object, ShouldEqual, path)
   152  
   153  				content := []byte("this is a blob")
   154  				digest := godigest.FromBytes(content)
   155  				So(digest, ShouldNotBeNil)
   156  
   157  				// blob upload
   158  				resp, err = resty.R().SetQueryParam("digest", digest.String()).
   159  					SetBasicAuth(username, password).
   160  					SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
   161  				So(err, ShouldBeNil)
   162  				So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
   163  				blobLoc := test.Location(baseURL, resp)
   164  				So(blobLoc, ShouldNotBeEmpty)
   165  				So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
   166  
   167  				// wait until the file is populated
   168  				byteValue, _ = io.ReadAll(auditFile)
   169  				for {
   170  					if len(byteValue) != 0 {
   171  						break
   172  					}
   173  					time.Sleep(100 * time.Millisecond)
   174  					byteValue, _ = io.ReadAll(auditFile)
   175  				}
   176  
   177  				err = json.Unmarshal(byteValue, &auditLog)
   178  				if err != nil {
   179  					panic(err)
   180  				}
   181  
   182  				So(auditLog.Subject, ShouldEqual, username)
   183  				So(auditLog.Action, ShouldEqual, http.MethodPut)
   184  				So(auditLog.Status, ShouldEqual, http.StatusCreated)
   185  
   186  				putPath := location + "?digest=" + strings.ReplaceAll(digest.String(), ":", "%3A")
   187  				So(auditLog.Object, ShouldEqual, putPath)
   188  
   189  				// delete this blob
   190  				resp, err = resty.R().SetBasicAuth(username, password).Delete(blobLoc)
   191  				So(err, ShouldBeNil)
   192  				So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
   193  				So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   194  
   195  				// wait until the file is populated
   196  				byteValue, _ = io.ReadAll(auditFile)
   197  				for {
   198  					if len(byteValue) != 0 {
   199  						break
   200  					}
   201  					time.Sleep(100 * time.Millisecond)
   202  					byteValue, _ = io.ReadAll(auditFile)
   203  				}
   204  
   205  				err = json.Unmarshal(byteValue, &auditLog)
   206  				if err != nil {
   207  					panic(err)
   208  				}
   209  
   210  				So(auditLog.Subject, ShouldEqual, username)
   211  				So(auditLog.Action, ShouldEqual, http.MethodDelete)
   212  				So(auditLog.Status, ShouldEqual, http.StatusAccepted)
   213  
   214  				deletePath := strings.ReplaceAll(path, "uploads/", digest.String())
   215  				So(auditLog.Object, ShouldEqual, deletePath)
   216  			})
   217  
   218  			Convey("Test PATCH request", func() {
   219  				path := "/v2/repo/blobs/uploads/"
   220  				resp, err := resty.R().SetBasicAuth(username, password).Post(baseURL + path)
   221  				So(err, ShouldBeNil)
   222  				So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
   223  				loc := test.Location(baseURL, resp)
   224  				So(loc, ShouldNotBeEmpty)
   225  				location := resp.Header().Get("Location")
   226  				So(location, ShouldNotBeEmpty)
   227  
   228  				// wait until the file is populated
   229  				byteValue, _ := io.ReadAll(auditFile)
   230  				for {
   231  					if len(byteValue) != 0 {
   232  						break
   233  					}
   234  					time.Sleep(100 * time.Millisecond)
   235  					byteValue, _ = io.ReadAll(auditFile)
   236  				}
   237  
   238  				var auditLog AuditLog
   239  				err = json.Unmarshal(byteValue, &auditLog)
   240  				if err != nil {
   241  					panic(err)
   242  				}
   243  
   244  				So(auditLog.Subject, ShouldEqual, username)
   245  				So(auditLog.Action, ShouldEqual, http.MethodPost)
   246  				So(auditLog.Status, ShouldEqual, http.StatusAccepted)
   247  				So(auditLog.Object, ShouldEqual, path)
   248  
   249  				var buf bytes.Buffer
   250  				chunk := []byte("this is a chunk")
   251  				n, err := buf.Write(chunk)
   252  				So(n, ShouldEqual, len(chunk))
   253  				So(err, ShouldBeNil)
   254  
   255  				// write a chunk
   256  				contentRange := fmt.Sprintf("%d-%d", 0, len(chunk)-1)
   257  				resp, err = resty.R().SetBasicAuth(username, password).
   258  					SetHeader("Content-Type", "application/octet-stream").
   259  					SetHeader("Content-Range", contentRange).SetBody(chunk).Patch(loc)
   260  				So(err, ShouldBeNil)
   261  				So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
   262  
   263  				// wait until the file is populated
   264  				byteValue, _ = io.ReadAll(auditFile)
   265  				for {
   266  					if len(byteValue) != 0 {
   267  						break
   268  					}
   269  					time.Sleep(100 * time.Millisecond)
   270  					byteValue, _ = io.ReadAll(auditFile)
   271  				}
   272  
   273  				err = json.Unmarshal(byteValue, &auditLog)
   274  				if err != nil {
   275  					panic(err)
   276  				}
   277  
   278  				So(auditLog.Subject, ShouldEqual, username)
   279  				So(auditLog.Action, ShouldEqual, http.MethodPatch)
   280  				So(auditLog.Status, ShouldEqual, http.StatusAccepted)
   281  
   282  				patchPath := location
   283  				So(auditLog.Object, ShouldEqual, patchPath)
   284  			})
   285  		})
   286  	})
   287  }
   288  
   289  func TestLogErrors(t *testing.T) {
   290  	Convey("Get error with unknown log level", t, func() {
   291  		So(func() { _ = log.NewLogger("invalid", "test.out") }, ShouldPanic)
   292  	})
   293  
   294  	Convey("Get error when opening log file", t, func() {
   295  		dir := t.TempDir()
   296  		logPath := path.Join(dir, "logFile")
   297  		err := os.WriteFile(logPath, []byte{}, 0o000)
   298  		So(err, ShouldBeNil)
   299  		So(func() {
   300  			_ = log.NewLogger(zerolog.DebugLevel.String(), logPath)
   301  		}, ShouldPanic)
   302  	})
   303  }
   304  
   305  func TestNewAuditLogger(t *testing.T) {
   306  	Convey("Get error with unknown audit log level", t, func() {
   307  		So(func() { _ = log.NewAuditLogger("invalid", "test.out") }, ShouldPanic)
   308  	})
   309  
   310  	Convey("Get error when opening audit file", t, func() {
   311  		dir := t.TempDir()
   312  		logPath := path.Join(dir, "logFile")
   313  		err := os.WriteFile(logPath, []byte{}, 0o000)
   314  		So(err, ShouldBeNil)
   315  		So(func() {
   316  			_ = log.NewAuditLogger(zerolog.DebugLevel.String(), logPath)
   317  		}, ShouldPanic)
   318  	})
   319  }