diff --git a/README.md b/README.md index 8d10cc0..575da07 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Features * Files with a certain name, when placed in the root of a site, have special functions: - [Netlify `_redirects`][_redirects] file can be used to specify HTTP redirect and rewrite rules. The _git-pages_ implementation currently does not support placeholders, query parameters, or conditions, and may differ from Netlify in other minor ways. If you find that a supported `_redirects` file feature does not work the same as on Netlify, please file an issue. (Note that _git-pages_ does not perform URL normalization; `/foo` and `/foo/` are *not* the same, unlike with Netlify.) - [Netlify `_headers`][_headers] file can be used to specify custom HTTP response headers (if allowlisted by configuration). In particular, this is useful to enable [CORS requests][cors]. The _git-pages_ implementation may differ from Netlify in minor ways; if you find that a `_headers` file feature does not work the same as on Netlify, please file an issue. + - [Netlify `Basic-Auth:`][basic-auth] pseudo-header in the `_headers` file can be used to password-protect parts of a site, if enabled via the `[limits].allow-basic-auth` configuration option. **This is not a security feature: credentials are stored in cleartext and are accessible to anyone who can update the site. *Only* use it in low-stakes applications, e.g. preventing search engines from indexing parts of a site.** The authors of _git-pages_ shall not be held liable for any unauthorized information disclosures resulting from the use of this feature. * Incremental updates can be made using `PUT` or `PATCH` requests where the body contains an archive (both tar and zip are supported). - Any archive entry that is a symlink to `/git/blobs/` is replaced with an existing manifest entry for the same site whose git blob hash matches ``. If there is no existing manifest entry with the specified git hash, the update fails with a `422 Unprocessable Entity`. - For this error response only, if the negotiated content type is `application/vnd.git-pages.unresolved`, the response will contain the `` of each unresolved reference, one per line. @@ -98,6 +99,7 @@ Features [_redirects]: https://docs.netlify.com/manage/routing/redirects/overview/ [_headers]: https://docs.netlify.com/manage/routing/headers/ +[basic-auth]: https://docs.netlify.com/manage/security/secure-access-to-sites/basic-authentication-with-custom-http-headers/ [cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS [go-git-sha256]: https://github.com/go-git/go-git/issues/706 [whiteout]: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories diff --git a/conf/config.default.toml b/conf/config.default.toml index d98a489..239e647 100644 --- a/conf/config.default.toml +++ b/conf/config.default.toml @@ -1,4 +1,5 @@ -# git-pages configuration +# This is a configuration containing default values only. The `config.example.toml` file contains +# a configuration more useful for demonstration purposes. log-format = 'text' @@ -25,6 +26,7 @@ max-heap-size-ratio = 0.5 forbidden-domains = [] allowed-repository-url-prefixes = [] allowed-custom-headers = ['X-Clacks-Overhead'] +allow-basic-auth = false [audit] node-id = 0 @@ -33,29 +35,3 @@ include-ip = '' [observability] slow-response-threshold = '500ms' - -# [[wildcard]] -# domain = "codeberg.page" -# clone-url = "https://codeberg.org//.git" -# index-repo = "pages" -# index-repo-branch = "main" -# authorization = "forgejo" - -# [fallback] -# proxy-to = "https://codeberg.page" -# insecure = false - -# S3-compatible object storage backend -# Set [storage] type = "s3" to activate. -# [storage.s3] -# endpoint = "play.min.io" -# access-key-id = "Q3AM3UQ867SPQQA43P2F" -# secret-access-key = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" -# region = "us-east-1" -# bucket = "git-pages-demo" -# [storage.s3.blob-cache] -# max-size = "256MB" -# [storage.s3.site-cache] -# max-size = "16MB" -# max-age = "60s" -# max-stale = "1h" diff --git a/conf/config.example.toml b/conf/config.example.toml index 9af9ead..bf8340e 100644 --- a/conf/config.example.toml +++ b/conf/config.example.toml @@ -1,5 +1,5 @@ -# Unless otherwise noted, every value in this file is the same -# as the intrinsic default value. +# This is a configuration used for demonstration purposes. The `config.default.toml` file contains +# a configuration corresponding to default values only. log-format = "text" @@ -48,10 +48,12 @@ max-inline-file-size = "256B" git-large-object-threshold = "1M" max-symlink-depth = 16 update-timeout = "60s" +concurrent-uploads = 1024 max-heap-size-ratio = 0.5 # * RAM_size forbidden-domains = [] allowed-repository-url-prefixes = [] allowed-custom-headers = ["X-Clacks-Overhead"] +allow-basic-auth = false [audit] node-id = 0 diff --git a/src/auth.go b/src/auth.go index 310a83e..4101420 100644 --- a/src/auth.go +++ b/src/auth.go @@ -347,7 +347,7 @@ func authorizeCodebergPagesV2(r *http.Request) (*Authorization, error) { } // Checks whether an operation that enables enumerating site contents is allowed. -func AuthorizeMetadataRetrieval(r *http.Request) (*Authorization, error) { +func AuthorizeMetadataRetrieval(r *http.Request, hasBasicAuth bool) (*Authorization, error) { causes := []error{AuthError{http.StatusUnauthorized, "unauthorized"}} auth := authorizeInsecure(r) @@ -365,27 +365,32 @@ func AuthorizeMetadataRetrieval(r *http.Request) (*Authorization, error) { return auth, nil } - for _, pattern := range wildcards { - auth, err = authorizeWildcardMatchHost(r, pattern) - if err != nil && IsUnauthorized(err) { - causes = append(causes, err) - } else if err != nil { // bad request - return nil, err - } else { - logc.Printf(r.Context(), "auth: wildcard %s\n", pattern.GetHost()) - return auth, nil + // Normally, sites that correspond to a forge via a wildcard match are considered completely + // public and safe to retrieve without authorization. However, this is no longer the case if + // they have password-protected sections. + if !hasBasicAuth { + for _, pattern := range wildcards { + auth, err = authorizeWildcardMatchHost(r, pattern) + if err != nil && IsUnauthorized(err) { + causes = append(causes, err) + } else if err != nil { // bad request + return nil, err + } else { + logc.Printf(r.Context(), "auth: wildcard %s\n", pattern.GetHost()) + return auth, nil + } } - } - if config.Feature("codeberg-pages-compat") { - auth, err = authorizeCodebergPagesV2(r) - if err != nil && IsUnauthorized(err) { - causes = append(causes, err) - } else if err != nil { // bad request - return nil, err - } else { - logc.Printf(r.Context(), "auth: codeberg %s\n", r.Host) - return auth, nil + if config.Feature("codeberg-pages-compat") { + auth, err = authorizeCodebergPagesV2(r) + if err != nil && IsUnauthorized(err) { + causes = append(causes, err) + } else if err != nil { // bad request + return nil, err + } else { + logc.Printf(r.Context(), "auth: codeberg %s\n", r.Host) + return auth, nil + } } } diff --git a/src/config.go b/src/config.go index fe1af2d..4a99c87 100644 --- a/src/config.go +++ b/src/config.go @@ -146,6 +146,9 @@ type LimitsConfig struct { // e.g. `Foo-Bar`. Setting this option permits including this custom header in `_headers`, // unless it is fundamentally unsafe. AllowedCustomHeaders []string `toml:"allowed-custom-headers" default:"[\"X-Clacks-Overhead\"]"` + // Whether to allow Netlify-style credentials specified in a `Basic-Auth:` pseudo-header. + // These credentials are plaintext. + AllowBasicAuth bool `toml:"allow-basic-auth" default:"false"` } type AuditConfig struct { diff --git a/src/headers.go b/src/headers.go index c162568..006e8fd 100644 --- a/src/headers.go +++ b/src/headers.go @@ -15,6 +15,7 @@ import ( ) var ErrHeaderNotAllowed = errors.New("custom header not allowed") +var ErrBasicAuthNotAllowed = errors.New("basic authorization not allowed") const HeadersFileName string = "_headers" @@ -75,11 +76,18 @@ func validateHeaderRule(rule headers.Rule) error { if slices.Contains(unsafeHeaders, header) { return fmt.Errorf("rule sets header %q (fundamentally unsafe)", header) } - if !slices.Contains(config.Limits.AllowedCustomHeaders, header) { - return fmt.Errorf("rule sets header %q (not allowlisted)", header) - } - if !IsAllowedCustomHeader(header) { // make sure we don't desync - panic(errors.New("header check inconsistency")) + switch header { + case "Basic-Auth": + if !config.Limits.AllowBasicAuth { + return fmt.Errorf("rule sets header %q (forbidden by policy)", header) + } + default: + if !slices.Contains(config.Limits.AllowedCustomHeaders, header) { + return fmt.Errorf("rule sets header %q (not allowlisted)", header) + } + if !IsAllowedCustomHeader(header) { // make sure we don't desync + panic(errors.New("header check inconsistency")) + } } } return nil @@ -114,16 +122,52 @@ func ProcessHeadersFile(ctx context.Context, manifest *Manifest) error { continue } headerMap := []*Header{} + credentials := []*BasicCredential{} + hasBasicAuth := false for header, values := range rule.Headers { - headerMap = append(headerMap, &Header{ - Name: proto.String(header), - Values: values, - }) + switch header { + case "Basic-Auth": + hasBasicAuth = true + for _, value := range values { + for _, usernamePassword := range strings.Split(value, " ") { + if usernamePassword == "" { + continue + } + if username, password, found := strings.Cut(usernamePassword, ":"); !found { + AddProblem(manifest, HeadersFileName, + "rule #%d %q: malformed Basic-Auth credential", index+1, rule.Path) + continue + } else { + credentials = append(credentials, &BasicCredential{ + Username: proto.String(username), + Password: proto.String(password), + }) + } + } + } + default: + headerMap = append(headerMap, &Header{ + Name: proto.String(header), + Values: values, + }) + } } + // Note that we may add an empty `headerMap` here even if only credentials are defined. + // This is intentional: in `_headers` files processing terminates at the first matching + // clause, and Netlify mixes Basic-Auth with all the other headers. manifest.Headers = append(manifest.Headers, &HeaderRule{ Path: proto.String(rule.Path), HeaderMap: headerMap, }) + // We're using `hasBasicAuth` instead of `len(credentials) > 0` so that if a `_headers` + // file defines only malformed credentials, we still add a rule (that in effect always + // denies access). + if hasBasicAuth { + manifest.BasicAuth = append(manifest.BasicAuth, &BasicAuthRule{ + Path: proto.String(rule.Path), + Credentials: credentials, + }) + } } return nil } @@ -143,13 +187,14 @@ func CollectHeadersFile(manifest *Manifest) string { return headers.Must(headers.UnparseString(headersRules)) } -func ApplyHeaderRules(manifest *Manifest, url *url.URL) (headers http.Header, err error) { - headers = http.Header{} +func matchPathRules[ + Rule interface{ GetPath() string }, +](rules []Rule, url *url.URL) (matched Rule) { fromSegments := pathSegments(url.Path) next: - for _, rule := range manifest.Headers { + for _, rule := range rules { // check if the rule matches url - ruleURL, _ := url.Parse(*rule.Path) // pre-validated in `validateHeaderRule` + ruleURL, _ := url.Parse(rule.GetPath()) // pre-validated in `validateHeaderRule` ruleSegments := pathSegments(ruleURL.Path) if ruleSegments[len(ruleSegments)-1] != "*" { if len(ruleSegments) < len(fromSegments) { @@ -167,8 +212,19 @@ next: continue next } } + matched = rule + break + } + return +} + +func ApplyHeaderRules(manifest *Manifest, url *url.URL) ( + headers http.Header, err error, +) { + headers = http.Header{} + if rule := matchPathRules(manifest.Headers, url); rule != nil { // the rule has matched url, validate headers against up-to-date policy - for _, header := range rule.HeaderMap { + for _, header := range rule.GetHeaderMap() { name := header.GetName() if !IsAllowedCustomHeader(name) { return nil, fmt.Errorf("%w: %s", ErrHeaderNotAllowed, name) @@ -177,7 +233,30 @@ next: headers.Add(name, value) } } - break } return } + +func ApplyBasicAuthRules(manifest *Manifest, url *url.URL, r *http.Request) (bool, error) { + if rule := matchPathRules(manifest.BasicAuth, url); rule == nil { + // no matches, authorized by default + return true, nil + } else { + // the rule has matched url, check that basic auth is allowed per up-to-date policy + if !config.Limits.AllowBasicAuth { + // basic auth configured in the past but not allowed any more + return false, ErrBasicAuthNotAllowed + } + if username, password, ok := r.BasicAuth(); ok { + // request has credentials, check them + for _, credential := range rule.GetCredentials() { + if credential.GetUsername() == username && credential.GetPassword() == password { + // authorized! + return true, nil + } + } + } + // request has no credentials, unauthorized + return false, nil + } +} diff --git a/src/manifest.go b/src/manifest.go index 2ed8313..88e6067 100644 --- a/src/manifest.go +++ b/src/manifest.go @@ -160,6 +160,10 @@ func IndexManifestByGitHash(manifest *Manifest) map[string]*Entry { return index } +func ManifestHasBasicAuth(manifest *Manifest) bool { + return len(manifest.GetBasicAuth()) > 0 +} + func IsEntryRegularFile(entry *Entry) bool { return entry.GetType() == Type_InlineFile || entry.GetType() == Type_ExternalFile @@ -371,19 +375,11 @@ func StoreManifest( span, ctx := ObserveFunction(ctx, "StoreManifest", "manifest.name", name) defer span.Finish() + extManifest := &Manifest{} + proto.Merge(extManifest, manifest) + // Replace inline files over certain size with references to external data. - extManifest := Manifest{ - RepoUrl: manifest.RepoUrl, - Branch: manifest.Branch, - Commit: manifest.Commit, - Contents: make(map[string]*Entry), - Redirects: manifest.Redirects, - Headers: manifest.Headers, - Problems: manifest.Problems, - OriginalSize: manifest.OriginalSize, - CompressedSize: manifest.CompressedSize, - StoredSize: proto.Int64(0), - } + extManifest.Contents = make(map[string]*Entry) for name, entry := range manifest.Contents { cannotBeInlined := entry.GetType() == Type_InlineFile && entry.GetCompressedSize() > int64(config.Limits.MaxInlineFileSize.Bytes()) @@ -419,12 +415,13 @@ func StoreManifest( config.Limits.MaxSiteSize.HR(), ) } + extManifest.StoredSize = proto.Int64(0) for _, blobSize := range blobSizes { *extManifest.StoredSize += blobSize } // Upload the resulting manifest and the blob it references. - extManifestData := EncodeManifest(&extManifest) + extManifestData := EncodeManifest(extManifest) if uint64(len(extManifestData)) > config.Limits.MaxManifestSize.Bytes() { return nil, fmt.Errorf("%w: manifest size %s exceeds %s limit", ErrManifestTooLarge, @@ -433,7 +430,7 @@ func StoreManifest( ) } - if err := backend.StageManifest(ctx, &extManifest); err != nil { + if err := backend.StageManifest(ctx, extManifest); err != nil { return nil, fmt.Errorf("stage manifest: %w", err) } @@ -460,7 +457,7 @@ func StoreManifest( return nil, err // currently ignores all but 1st error } - if err := backend.CommitManifest(ctx, name, &extManifest, opts); err != nil { + if err := backend.CommitManifest(ctx, name, extManifest, opts); err != nil { if errors.Is(err, ErrDomainFrozen) { return nil, err } else { @@ -468,5 +465,5 @@ func StoreManifest( } } - return &extManifest, nil + return extManifest, nil } diff --git a/src/pages.go b/src/pages.go index 45c1942..0c21a75 100644 --- a/src/pages.go +++ b/src/pages.go @@ -193,8 +193,8 @@ func getPage(w http.ResponseWriter, r *http.Request) error { case metadataPath == "manifest.json": // metadata requests require authorization to avoid making pushes from private - // repositories enumerable - _, err := AuthorizeMetadataRetrieval(r) + // repositories enumerable or exposing basic-auth protected sections + _, err := AuthorizeMetadataRetrieval(r, ManifestHasBasicAuth(manifest)) if err != nil { return err } @@ -208,7 +208,7 @@ func getPage(w http.ResponseWriter, r *http.Request) error { case metadataPath == "archive.tar": // same as above - _, err := AuthorizeMetadataRetrieval(r) + _, err := AuthorizeMetadataRetrieval(r, ManifestHasBasicAuth(manifest)) if err != nil { return err } @@ -244,6 +244,19 @@ func getPage(w http.ResponseWriter, r *http.Request) error { } } + // Apply basic-auth rules before checking existence of a path to avoid leaking the latter. + authorized, err := ApplyBasicAuthRules(manifest, &url.URL{Path: sitePath}, r) + if err != nil { + // See comment below for the error case under `ApplyHeaderRules`. + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "%s\n", err) + return err + } else if !authorized { + w.Header().Set("WWW-Authenticate", `Basic charset="UTF-8"`) + w.WriteHeader(http.StatusUnauthorized) + return nil + } + entryPath := sitePath entry := (*Entry)(nil) appliedRedirect := false diff --git a/src/schema.pb.go b/src/schema.pb.go index 63b9532..bf8fb04 100644 --- a/src/schema.pb.go +++ b/src/schema.pb.go @@ -479,6 +479,110 @@ func (x *HeaderRule) GetHeaderMap() []*Header { return nil } +type BasicCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username *string `protobuf:"bytes,1,opt,name=username" json:"username,omitempty"` + Password *string `protobuf:"bytes,2,opt,name=password" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BasicCredential) Reset() { + *x = BasicCredential{} + mi := &file_schema_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BasicCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BasicCredential) ProtoMessage() {} + +func (x *BasicCredential) ProtoReflect() protoreflect.Message { + mi := &file_schema_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BasicCredential.ProtoReflect.Descriptor instead. +func (*BasicCredential) Descriptor() ([]byte, []int) { + return file_schema_proto_rawDescGZIP(), []int{4} +} + +func (x *BasicCredential) GetUsername() string { + if x != nil && x.Username != nil { + return *x.Username + } + return "" +} + +func (x *BasicCredential) GetPassword() string { + if x != nil && x.Password != nil { + return *x.Password + } + return "" +} + +type BasicAuthRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path *string `protobuf:"bytes,1,opt,name=path" json:"path,omitempty"` + Credentials []*BasicCredential `protobuf:"bytes,2,rep,name=credentials" json:"credentials,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BasicAuthRule) Reset() { + *x = BasicAuthRule{} + mi := &file_schema_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BasicAuthRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BasicAuthRule) ProtoMessage() {} + +func (x *BasicAuthRule) ProtoReflect() protoreflect.Message { + mi := &file_schema_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BasicAuthRule.ProtoReflect.Descriptor instead. +func (*BasicAuthRule) Descriptor() ([]byte, []int) { + return file_schema_proto_rawDescGZIP(), []int{5} +} + +func (x *BasicAuthRule) GetPath() string { + if x != nil && x.Path != nil { + return *x.Path + } + return "" +} + +func (x *BasicAuthRule) GetCredentials() []*BasicCredential { + if x != nil { + return x.Credentials + } + return nil +} + type Problem struct { state protoimpl.MessageState `protogen:"open.v1"` Path *string `protobuf:"bytes,1,opt,name=path" json:"path,omitempty"` @@ -489,7 +593,7 @@ type Problem struct { func (x *Problem) Reset() { *x = Problem{} - mi := &file_schema_proto_msgTypes[4] + mi := &file_schema_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -501,7 +605,7 @@ func (x *Problem) String() string { func (*Problem) ProtoMessage() {} func (x *Problem) ProtoReflect() protoreflect.Message { - mi := &file_schema_proto_msgTypes[4] + mi := &file_schema_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -514,7 +618,7 @@ func (x *Problem) ProtoReflect() protoreflect.Message { // Deprecated: Use Problem.ProtoReflect.Descriptor instead. func (*Problem) Descriptor() ([]byte, []int) { - return file_schema_proto_rawDescGZIP(), []int{4} + return file_schema_proto_rawDescGZIP(), []int{6} } func (x *Problem) GetPath() string { @@ -543,8 +647,9 @@ type Manifest struct { CompressedSize *int64 `protobuf:"varint,5,opt,name=compressed_size,json=compressedSize" json:"compressed_size,omitempty"` // sum of each `entry.compressed_size` StoredSize *int64 `protobuf:"varint,8,opt,name=stored_size,json=storedSize" json:"stored_size,omitempty"` // sum of deduplicated `entry.compressed_size` for external files only // Netlify-style `_redirects` and `_headers` rules. - Redirects []*RedirectRule `protobuf:"bytes,6,rep,name=redirects" json:"redirects,omitempty"` - Headers []*HeaderRule `protobuf:"bytes,9,rep,name=headers" json:"headers,omitempty"` + Redirects []*RedirectRule `protobuf:"bytes,6,rep,name=redirects" json:"redirects,omitempty"` + Headers []*HeaderRule `protobuf:"bytes,9,rep,name=headers" json:"headers,omitempty"` + BasicAuth []*BasicAuthRule `protobuf:"bytes,11,rep,name=basic_auth,json=basicAuth" json:"basic_auth,omitempty"` // Diagnostics for non-fatal errors. Problems []*Problem `protobuf:"bytes,7,rep,name=problems" json:"problems,omitempty"` unknownFields protoimpl.UnknownFields @@ -553,7 +658,7 @@ type Manifest struct { func (x *Manifest) Reset() { *x = Manifest{} - mi := &file_schema_proto_msgTypes[5] + mi := &file_schema_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -565,7 +670,7 @@ func (x *Manifest) String() string { func (*Manifest) ProtoMessage() {} func (x *Manifest) ProtoReflect() protoreflect.Message { - mi := &file_schema_proto_msgTypes[5] + mi := &file_schema_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -578,7 +683,7 @@ func (x *Manifest) ProtoReflect() protoreflect.Message { // Deprecated: Use Manifest.ProtoReflect.Descriptor instead. func (*Manifest) Descriptor() ([]byte, []int) { - return file_schema_proto_rawDescGZIP(), []int{5} + return file_schema_proto_rawDescGZIP(), []int{7} } func (x *Manifest) GetRepoUrl() string { @@ -644,6 +749,13 @@ func (x *Manifest) GetHeaders() []*HeaderRule { return nil } +func (x *Manifest) GetBasicAuth() []*BasicAuthRule { + if x != nil { + return x.BasicAuth + } + return nil +} + func (x *Manifest) GetProblems() []*Problem { if x != nil { return x.Problems @@ -669,7 +781,7 @@ type AuditRecord struct { func (x *AuditRecord) Reset() { *x = AuditRecord{} - mi := &file_schema_proto_msgTypes[6] + mi := &file_schema_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -681,7 +793,7 @@ func (x *AuditRecord) String() string { func (*AuditRecord) ProtoMessage() {} func (x *AuditRecord) ProtoReflect() protoreflect.Message { - mi := &file_schema_proto_msgTypes[6] + mi := &file_schema_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -694,7 +806,7 @@ func (x *AuditRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use AuditRecord.ProtoReflect.Descriptor instead. func (*AuditRecord) Descriptor() ([]byte, []int) { - return file_schema_proto_rawDescGZIP(), []int{6} + return file_schema_proto_rawDescGZIP(), []int{8} } func (x *AuditRecord) GetId() int64 { @@ -757,7 +869,7 @@ type Principal struct { func (x *Principal) Reset() { *x = Principal{} - mi := &file_schema_proto_msgTypes[7] + mi := &file_schema_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -769,7 +881,7 @@ func (x *Principal) String() string { func (*Principal) ProtoMessage() {} func (x *Principal) ProtoReflect() protoreflect.Message { - mi := &file_schema_proto_msgTypes[7] + mi := &file_schema_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -782,7 +894,7 @@ func (x *Principal) ProtoReflect() protoreflect.Message { // Deprecated: Use Principal.ProtoReflect.Descriptor instead. func (*Principal) Descriptor() ([]byte, []int) { - return file_schema_proto_rawDescGZIP(), []int{7} + return file_schema_proto_rawDescGZIP(), []int{9} } func (x *Principal) GetIpAddress() string { @@ -817,7 +929,7 @@ type ForgeUser struct { func (x *ForgeUser) Reset() { *x = ForgeUser{} - mi := &file_schema_proto_msgTypes[8] + mi := &file_schema_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -829,7 +941,7 @@ func (x *ForgeUser) String() string { func (*ForgeUser) ProtoMessage() {} func (x *ForgeUser) ProtoReflect() protoreflect.Message { - mi := &file_schema_proto_msgTypes[8] + mi := &file_schema_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -842,7 +954,7 @@ func (x *ForgeUser) ProtoReflect() protoreflect.Message { // Deprecated: Use ForgeUser.ProtoReflect.Descriptor instead. func (*ForgeUser) Descriptor() ([]byte, []int) { - return file_schema_proto_rawDescGZIP(), []int{8} + return file_schema_proto_rawDescGZIP(), []int{10} } func (x *ForgeUser) GetOrigin() string { @@ -892,10 +1004,16 @@ const file_schema_proto_rawDesc = "" + "HeaderRule\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12&\n" + "\n" + - "header_map\x18\x02 \x03(\v2\a.HeaderR\theaderMap\"3\n" + + "header_map\x18\x02 \x03(\v2\a.HeaderR\theaderMap\"I\n" + + "\x0fBasicCredential\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\"W\n" + + "\rBasicAuthRule\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x122\n" + + "\vcredentials\x18\x02 \x03(\v2\x10.BasicCredentialR\vcredentials\"3\n" + "\aProblem\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n" + - "\x05cause\x18\x02 \x01(\tR\x05cause\"\xb8\x03\n" + + "\x05cause\x18\x02 \x01(\tR\x05cause\"\xe7\x03\n" + "\bManifest\x12\x19\n" + "\brepo_url\x18\x01 \x01(\tR\arepoUrl\x12\x16\n" + "\x06branch\x18\x02 \x01(\tR\x06branch\x12\x16\n" + @@ -907,7 +1025,9 @@ const file_schema_proto_rawDesc = "" + "\vstored_size\x18\b \x01(\x03R\n" + "storedSize\x12+\n" + "\tredirects\x18\x06 \x03(\v2\r.RedirectRuleR\tredirects\x12%\n" + - "\aheaders\x18\t \x03(\v2\v.HeaderRuleR\aheaders\x12$\n" + + "\aheaders\x18\t \x03(\v2\v.HeaderRuleR\aheaders\x12-\n" + + "\n" + + "basic_auth\x18\v \x03(\v2\x0e.BasicAuthRuleR\tbasicAuth\x12$\n" + "\bproblems\x18\a \x03(\v2\b.ProblemR\bproblems\x1aC\n" + "\rContentsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x1c\n" + @@ -964,7 +1084,7 @@ func file_schema_proto_rawDescGZIP() []byte { } var file_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_schema_proto_goTypes = []any{ (Type)(0), // 0: Type (Transform)(0), // 1: Transform @@ -973,33 +1093,37 @@ var file_schema_proto_goTypes = []any{ (*RedirectRule)(nil), // 4: RedirectRule (*Header)(nil), // 5: Header (*HeaderRule)(nil), // 6: HeaderRule - (*Problem)(nil), // 7: Problem - (*Manifest)(nil), // 8: Manifest - (*AuditRecord)(nil), // 9: AuditRecord - (*Principal)(nil), // 10: Principal - (*ForgeUser)(nil), // 11: ForgeUser - nil, // 12: Manifest.ContentsEntry - (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp + (*BasicCredential)(nil), // 7: BasicCredential + (*BasicAuthRule)(nil), // 8: BasicAuthRule + (*Problem)(nil), // 9: Problem + (*Manifest)(nil), // 10: Manifest + (*AuditRecord)(nil), // 11: AuditRecord + (*Principal)(nil), // 12: Principal + (*ForgeUser)(nil), // 13: ForgeUser + nil, // 14: Manifest.ContentsEntry + (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp } var file_schema_proto_depIdxs = []int32{ 0, // 0: Entry.type:type_name -> Type 1, // 1: Entry.transform:type_name -> Transform 5, // 2: HeaderRule.header_map:type_name -> Header - 12, // 3: Manifest.contents:type_name -> Manifest.ContentsEntry - 4, // 4: Manifest.redirects:type_name -> RedirectRule - 6, // 5: Manifest.headers:type_name -> HeaderRule - 7, // 6: Manifest.problems:type_name -> Problem - 13, // 7: AuditRecord.timestamp:type_name -> google.protobuf.Timestamp - 2, // 8: AuditRecord.event:type_name -> AuditEvent - 10, // 9: AuditRecord.principal:type_name -> Principal - 8, // 10: AuditRecord.manifest:type_name -> Manifest - 11, // 11: Principal.forge_user:type_name -> ForgeUser - 3, // 12: Manifest.ContentsEntry.value:type_name -> Entry - 13, // [13:13] is the sub-list for method output_type - 13, // [13:13] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 7, // 3: BasicAuthRule.credentials:type_name -> BasicCredential + 14, // 4: Manifest.contents:type_name -> Manifest.ContentsEntry + 4, // 5: Manifest.redirects:type_name -> RedirectRule + 6, // 6: Manifest.headers:type_name -> HeaderRule + 8, // 7: Manifest.basic_auth:type_name -> BasicAuthRule + 9, // 8: Manifest.problems:type_name -> Problem + 15, // 9: AuditRecord.timestamp:type_name -> google.protobuf.Timestamp + 2, // 10: AuditRecord.event:type_name -> AuditEvent + 12, // 11: AuditRecord.principal:type_name -> Principal + 10, // 12: AuditRecord.manifest:type_name -> Manifest + 13, // 13: Principal.forge_user:type_name -> ForgeUser + 3, // 14: Manifest.ContentsEntry.value:type_name -> Entry + 15, // [15:15] is the sub-list for method output_type + 15, // [15:15] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_schema_proto_init() } @@ -1013,7 +1137,7 @@ func file_schema_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_schema_proto_rawDesc), len(file_schema_proto_rawDesc)), NumEnums: 3, - NumMessages: 10, + NumMessages: 12, NumExtensions: 0, NumServices: 0, }, diff --git a/src/schema.proto b/src/schema.proto index c9f2f1b..867beb5 100644 --- a/src/schema.proto +++ b/src/schema.proto @@ -76,6 +76,16 @@ message HeaderRule { repeated Header header_map = 2; } +message BasicCredential { + string username = 1; + string password = 2; +} + +message BasicAuthRule { + string path = 1; + repeated BasicCredential credentials = 2; +} + message Problem { string path = 1; string cause = 2; @@ -96,6 +106,7 @@ message Manifest { // Netlify-style `_redirects` and `_headers` rules. repeated RedirectRule redirects = 6; repeated HeaderRule headers = 9; + repeated BasicAuthRule basic_auth = 11; // Diagnostics for non-fatal errors. repeated Problem problems = 7;