Add support for Netlify Basic-Auth: mechanism.

This commit is contained in:
Catherine
2026-03-29 12:00:22 +00:00
parent 38eb8afd0e
commit 5258bf756b
10 changed files with 339 additions and 127 deletions

View File

@@ -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/<git-sha256>` is replaced with an existing manifest entry for the same site whose git blob hash matches `<git-sha256>`. 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 `<git-sha256>` 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

View File

@@ -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/<user>/<project>.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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
},

View File

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