[mirror] Scalable static site server for Git forges (like GitHub Pages)

Record the authorized forge user's name in the audit log.

miyuko 9e966401 3e377986

+184 -24
+6
src/audit.go
··· 102 102 if record.Principal.GetIpAddress() != "" { 103 103 items = append(items, record.Principal.GetIpAddress()) 104 104 } 105 + if record.Principal.GetForgeUser() != nil { 106 + items = append(items, fmt.Sprintf("%s/%s(%d)", 107 + record.Principal.GetForgeUser().GetOrigin(), 108 + record.Principal.GetForgeUser().GetHandle(), 109 + record.Principal.GetForgeUser().GetId())) 110 + } 105 111 if record.Principal.GetCliAdmin() { 106 112 items = append(items, "<cli-admin>") 107 113 }
+71 -5
src/auth.go
··· 106 106 repoURLs []string 107 107 // Only the exact branch is allowed. 108 108 branch string 109 + // The authorized forge user. 110 + forgeUser *ForgeUser 109 111 } 110 112 111 113 func authorizeDNSChallenge(r *http.Request) (*Authorization, error) { ··· 266 268 267 269 if userName, found := pattern.Matches(host); found { 268 270 repoURL, branch := pattern.ApplyTemplate(userName, projectName) 269 - return &Authorization{[]string{repoURL}, branch}, nil 271 + return &Authorization{repoURLs: []string{repoURL}, branch: branch}, nil 270 272 } else { 271 273 return nil, AuthError{ 272 274 http.StatusUnauthorized, ··· 606 608 return nil 607 609 } 608 610 611 + // Gogs, Gitea, and Forgejo all support the same API here. 612 + func fetchGogsAuthorizedUser(baseURL *url.URL, authorization string) (*ForgeUser, error) { 613 + request, err := http.NewRequest("GET", baseURL.JoinPath("/api/v1/user").String(), nil) 614 + if err != nil { 615 + panic(err) // misconfiguration 616 + } 617 + request.Header.Set("Accept", "application/json") 618 + request.Header.Set("Authorization", authorization) 619 + 620 + httpClient := http.Client{Timeout: 5 * time.Second} 621 + response, err := httpClient.Do(request) 622 + if err != nil { 623 + return nil, AuthError{ 624 + http.StatusServiceUnavailable, 625 + fmt.Sprintf("cannot fetch authorized forge user: %s", err), 626 + } 627 + } 628 + defer response.Body.Close() 629 + 630 + if response.StatusCode != http.StatusOK { 631 + return nil, AuthError{ 632 + http.StatusServiceUnavailable, 633 + fmt.Sprintf( 634 + "cannot fetch authorized forge user: GET %s returned %s", 635 + request.URL, 636 + response.Status, 637 + ), 638 + } 639 + } 640 + decoder := json.NewDecoder(response.Body) 641 + 642 + var userInfo struct { 643 + ID int64 644 + Login string 645 + } 646 + if err = decoder.Decode(&userInfo); err != nil { 647 + return nil, errors.Join(AuthError{ 648 + http.StatusServiceUnavailable, 649 + fmt.Sprintf( 650 + "cannot fetch authorized forge user: GET %s returned malformed JSON", 651 + request.URL, 652 + ), 653 + }, err) 654 + } 655 + 656 + origin := request.URL.Hostname() 657 + return &ForgeUser{ 658 + Origin: &origin, 659 + Id: &userInfo.ID, 660 + Handle: &userInfo.Login, 661 + }, nil 662 + } 663 + 609 664 func authorizeForgeWithToken(r *http.Request) (*Authorization, error) { 610 665 authorization := r.Header.Get("Forge-Authorization") 611 666 if authorization == "" { ··· 640 695 continue 641 696 } 642 697 643 - // This will actually be ignored by the callers of AuthorizeUpdateFromArchive and 644 - // AuthorizeDeletion, but we return this information as it makes sense to do 645 - // contextually here. 646 - return &Authorization{[]string{repoURL}, branch}, nil 698 + authorizedUser, err := fetchGogsAuthorizedUser(parsedRepoURL, authorization) 699 + if err != nil { 700 + errs = append(errs, err) 701 + continue 702 + } 703 + 704 + return &Authorization{ 705 + // This will actually be ignored by the callers of AuthorizeUpdateFromArchive and 706 + // AuthorizeDeletion, but we return this information as it makes sense to do 707 + // contextually here. 708 + repoURLs: []string{repoURL}, 709 + branch: branch, 710 + 711 + forgeUser: authorizedUser, 712 + }, nil 647 713 } 648 714 } 649 715
+9 -5
src/pages.go
··· 499 499 result = UpdateFromRepository(ctx, webRoot, repoURL, branch) 500 500 501 501 default: 502 - _, err := AuthorizeUpdateFromArchive(r) 503 - if err != nil { 502 + if auth, err := AuthorizeUpdateFromArchive(r); err != nil { 504 503 return err 504 + } else if auth.forgeUser != nil { 505 + GetPrincipal(r.Context()).ForgeUser = auth.forgeUser 505 506 } 506 507 507 508 if checkDryRun(w, r) { ··· 531 532 return err 532 533 } 533 534 534 - if _, err = AuthorizeUpdateFromArchive(r); err != nil { 535 + if auth, err := AuthorizeUpdateFromArchive(r); err != nil { 535 536 return err 537 + } else if auth.forgeUser != nil { 538 + GetPrincipal(r.Context()).ForgeUser = auth.forgeUser 536 539 } 537 540 538 541 if checkDryRun(w, r) { ··· 661 664 return err 662 665 } 663 666 664 - _, err = AuthorizeDeletion(r) 665 - if err != nil { 667 + if auth, err := AuthorizeDeletion(r); err != nil { 666 668 return err 669 + } else if auth.forgeUser != nil { 670 + GetPrincipal(r.Context()).ForgeUser = auth.forgeUser 667 671 } 668 672 669 673 if checkDryRun(w, r) {
+91 -14
src/schema.pb.go
··· 750 750 state protoimpl.MessageState `protogen:"open.v1"` 751 751 IpAddress *string `protobuf:"bytes,1,opt,name=ip_address,json=ipAddress" json:"ip_address,omitempty"` 752 752 CliAdmin *bool `protobuf:"varint,2,opt,name=cli_admin,json=cliAdmin" json:"cli_admin,omitempty"` 753 + ForgeUser *ForgeUser `protobuf:"bytes,3,opt,name=forge_user,json=forgeUser" json:"forge_user,omitempty"` 753 754 unknownFields protoimpl.UnknownFields 754 755 sizeCache protoimpl.SizeCache 755 756 } ··· 798 799 return false 799 800 } 800 801 802 + func (x *Principal) GetForgeUser() *ForgeUser { 803 + if x != nil { 804 + return x.ForgeUser 805 + } 806 + return nil 807 + } 808 + 809 + type ForgeUser struct { 810 + state protoimpl.MessageState `protogen:"open.v1"` 811 + Origin *string `protobuf:"bytes,1,opt,name=origin" json:"origin,omitempty"` 812 + Id *int64 `protobuf:"varint,2,opt,name=id" json:"id,omitempty"` 813 + Handle *string `protobuf:"bytes,3,opt,name=handle" json:"handle,omitempty"` 814 + unknownFields protoimpl.UnknownFields 815 + sizeCache protoimpl.SizeCache 816 + } 817 + 818 + func (x *ForgeUser) Reset() { 819 + *x = ForgeUser{} 820 + mi := &file_schema_proto_msgTypes[8] 821 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 822 + ms.StoreMessageInfo(mi) 823 + } 824 + 825 + func (x *ForgeUser) String() string { 826 + return protoimpl.X.MessageStringOf(x) 827 + } 828 + 829 + func (*ForgeUser) ProtoMessage() {} 830 + 831 + func (x *ForgeUser) ProtoReflect() protoreflect.Message { 832 + mi := &file_schema_proto_msgTypes[8] 833 + if x != nil { 834 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 835 + if ms.LoadMessageInfo() == nil { 836 + ms.StoreMessageInfo(mi) 837 + } 838 + return ms 839 + } 840 + return mi.MessageOf(x) 841 + } 842 + 843 + // Deprecated: Use ForgeUser.ProtoReflect.Descriptor instead. 844 + func (*ForgeUser) Descriptor() ([]byte, []int) { 845 + return file_schema_proto_rawDescGZIP(), []int{8} 846 + } 847 + 848 + func (x *ForgeUser) GetOrigin() string { 849 + if x != nil && x.Origin != nil { 850 + return *x.Origin 851 + } 852 + return "" 853 + } 854 + 855 + func (x *ForgeUser) GetId() int64 { 856 + if x != nil && x.Id != nil { 857 + return *x.Id 858 + } 859 + return 0 860 + } 861 + 862 + func (x *ForgeUser) GetHandle() string { 863 + if x != nil && x.Handle != nil { 864 + return *x.Handle 865 + } 866 + return "" 867 + } 868 + 801 869 var File_schema_proto protoreflect.FileDescriptor 802 870 803 871 const file_schema_proto_rawDesc = "" + ··· 853 921 "\x06domain\x18\n" + 854 922 " \x01(\tR\x06domain\x12\x18\n" + 855 923 "\aproject\x18\v \x01(\tR\aproject\x12%\n" + 856 - "\bmanifest\x18\f \x01(\v2\t.ManifestR\bmanifest\"G\n" + 924 + "\bmanifest\x18\f \x01(\v2\t.ManifestR\bmanifest\"r\n" + 857 925 "\tPrincipal\x12\x1d\n" + 858 926 "\n" + 859 927 "ip_address\x18\x01 \x01(\tR\tipAddress\x12\x1b\n" + 860 - "\tcli_admin\x18\x02 \x01(\bR\bcliAdmin*V\n" + 928 + "\tcli_admin\x18\x02 \x01(\bR\bcliAdmin\x12)\n" + 929 + "\n" + 930 + "forge_user\x18\x03 \x01(\v2\n" + 931 + ".ForgeUserR\tforgeUser\"K\n" + 932 + "\tForgeUser\x12\x16\n" + 933 + "\x06origin\x18\x01 \x01(\tR\x06origin\x12\x0e\n" + 934 + "\x02id\x18\x02 \x01(\x03R\x02id\x12\x16\n" + 935 + "\x06handle\x18\x03 \x01(\tR\x06handle*V\n" + 861 936 "\x04Type\x12\x10\n" + 862 937 "\fInvalidEntry\x10\x00\x12\r\n" + 863 938 "\tDirectory\x10\x01\x12\x0e\n" + ··· 889 964 } 890 965 891 966 var file_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 3) 892 - var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 9) 967 + var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 10) 893 968 var file_schema_proto_goTypes = []any{ 894 969 (Type)(0), // 0: Type 895 970 (Transform)(0), // 1: Transform ··· 902 977 (*Manifest)(nil), // 8: Manifest 903 978 (*AuditRecord)(nil), // 9: AuditRecord 904 979 (*Principal)(nil), // 10: Principal 905 - nil, // 11: Manifest.ContentsEntry 906 - (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp 980 + (*ForgeUser)(nil), // 11: ForgeUser 981 + nil, // 12: Manifest.ContentsEntry 982 + (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp 907 983 } 908 984 var file_schema_proto_depIdxs = []int32{ 909 985 0, // 0: Entry.type:type_name -> Type 910 986 1, // 1: Entry.transform:type_name -> Transform 911 987 5, // 2: HeaderRule.header_map:type_name -> Header 912 - 11, // 3: Manifest.contents:type_name -> Manifest.ContentsEntry 988 + 12, // 3: Manifest.contents:type_name -> Manifest.ContentsEntry 913 989 4, // 4: Manifest.redirects:type_name -> RedirectRule 914 990 6, // 5: Manifest.headers:type_name -> HeaderRule 915 991 7, // 6: Manifest.problems:type_name -> Problem 916 - 12, // 7: AuditRecord.timestamp:type_name -> google.protobuf.Timestamp 992 + 13, // 7: AuditRecord.timestamp:type_name -> google.protobuf.Timestamp 917 993 2, // 8: AuditRecord.event:type_name -> AuditEvent 918 994 10, // 9: AuditRecord.principal:type_name -> Principal 919 995 8, // 10: AuditRecord.manifest:type_name -> Manifest 920 - 3, // 11: Manifest.ContentsEntry.value:type_name -> Entry 921 - 12, // [12:12] is the sub-list for method output_type 922 - 12, // [12:12] is the sub-list for method input_type 923 - 12, // [12:12] is the sub-list for extension type_name 924 - 12, // [12:12] is the sub-list for extension extendee 925 - 0, // [0:12] is the sub-list for field type_name 996 + 11, // 11: Principal.forge_user:type_name -> ForgeUser 997 + 3, // 12: Manifest.ContentsEntry.value:type_name -> Entry 998 + 13, // [13:13] is the sub-list for method output_type 999 + 13, // [13:13] is the sub-list for method input_type 1000 + 13, // [13:13] is the sub-list for extension type_name 1001 + 13, // [13:13] is the sub-list for extension extendee 1002 + 0, // [0:13] is the sub-list for field type_name 926 1003 } 927 1004 928 1005 func init() { file_schema_proto_init() } ··· 936 1013 GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 937 1014 RawDescriptor: unsafe.Slice(unsafe.StringData(file_schema_proto_rawDesc), len(file_schema_proto_rawDesc)), 938 1015 NumEnums: 3, 939 - NumMessages: 9, 1016 + NumMessages: 10, 940 1017 NumExtensions: 0, 941 1018 NumServices: 0, 942 1019 },
+7
src/schema.proto
··· 132 132 message Principal { 133 133 string ip_address = 1; 134 134 bool cli_admin = 2; 135 + ForgeUser forge_user = 3; 136 + } 137 + 138 + message ForgeUser { 139 + string origin = 1; 140 + int64 id = 2; 141 + string handle = 3; 135 142 }