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 }