From 4e6a782a8945cf4074da57d990ff8e847d4e6482 Mon Sep 17 00:00:00 2001 From: Florian Pallas Date: Thu, 9 Apr 2026 19:38:33 +0200 Subject: [PATCH] feat: add admin views for federation configuration, hosts and users (#11115) Fixes #9282 Adds a new admin panel category for federation related administration. Includes views for: - Instance Federation Configuration - List of Federation Hosts - (Per-Instance) List of Federated Users Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11115 Reviewed-by: elle <0xllx0@noreply.codeberg.org> Reviewed-by: Panagiotis "Ivory" Vasilopoulos Reviewed-by: Gusted Co-authored-by: Florian Pallas Co-committed-by: Florian Pallas --- models/forgefed/federationhost_repository.go | 25 ++ models/user/user_repository.go | 51 ++++ modules/setting/ui.go | 30 ++- options/locale_next/locale_en-US.json | 31 +++ routers/web/admin/config.go | 2 + routers/web/admin/federation_host.go | 65 ++++++ routers/web/admin/federation_hosts.go | 58 +++++ routers/web/admin/federation_users.go | 56 +++++ routers/web/web.go | 8 + services/context/context.go | 1 + templates/admin/config.tmpl | 27 +++ templates/admin/federation/host.tmpl | 39 ++++ templates/admin/federation/hosts.tmpl | 56 +++++ templates/admin/federation/user_list.tmpl | 28 +++ templates/admin/federation/users.tmpl | 11 + templates/admin/navbar.tmpl | 13 ++ tests/integration/admin_federation_test.go | 128 ++++++++++ .../federated_user.yml | 17 ++ .../federation_host.yml | 13 ++ .../user.yml | 107 +++++++++ tests/integration/repo_forgefed_test.go | 219 ++++++++++++++++++ 21 files changed, 973 insertions(+), 12 deletions(-) create mode 100644 routers/web/admin/federation_host.go create mode 100644 routers/web/admin/federation_hosts.go create mode 100644 routers/web/admin/federation_users.go create mode 100644 templates/admin/federation/host.tmpl create mode 100644 templates/admin/federation/hosts.tmpl create mode 100644 templates/admin/federation/user_list.tmpl create mode 100644 templates/admin/federation/users.tmpl create mode 100644 tests/integration/admin_federation_test.go create mode 100644 tests/integration/fixtures/TestAdminFederationViewHostsAndUsers/federated_user.yml create mode 100644 tests/integration/fixtures/TestAdminFederationViewHostsAndUsers/federation_host.yml create mode 100644 tests/integration/fixtures/TestAdminFederationViewHostsAndUsers/user.yml create mode 100644 tests/integration/repo_forgefed_test.go diff --git a/models/forgefed/federationhost_repository.go b/models/forgefed/federationhost_repository.go index a44b502ba1..c889c1140b 100644 --- a/models/forgefed/federationhost_repository.go +++ b/models/forgefed/federationhost_repository.go @@ -16,6 +16,31 @@ func init() { db.RegisterModel(new(FederationHost)) } +func CountFederationHosts(ctx context.Context) (int64, error) { + return db.GetEngine(ctx).Count(FederationHost{}) +} + +func FindFederationHosts(ctx context.Context, opts db.ListOptions) (hosts []*FederationHost, err error) { + sess := db.GetEngine(ctx) + + if opts.PageSize > 0 { + sess = db.SetSessionPagination(sess, &opts) + } + + err = sess.Find(&hosts) + if err != nil { + return nil, err + } + + for _, host := range hosts { + if res, err := validation.IsValid(host); !res { + return nil, err + } + } + + return hosts, nil +} + func GetFederationHost(ctx context.Context, ID int64) (*FederationHost, error) { log.Trace("GetFederationHost: %v", ID) host := new(FederationHost) diff --git a/models/user/user_repository.go b/models/user/user_repository.go index 9692bd7304..1ec0906e4b 100644 --- a/models/user/user_repository.go +++ b/models/user/user_repository.go @@ -83,6 +83,57 @@ func FindFederatedUser(ctx context.Context, externalID string, federationHostID return user, federatedUser, nil } +func CountFederatedUsers(ctx context.Context) (int64, error) { + return db.GetEngine(ctx).Count(FederatedUser{}) +} + +func FindFederatedUsers(ctx context.Context, opts db.ListOptions) (users []*FederatedUser, err error) { + sess := db.GetEngine(ctx) + + if opts.PageSize > 0 { + sess = db.SetSessionPagination(sess, &opts) + } + + err = sess.Find(&users) + if err != nil { + return nil, err + } + + for _, user := range users { + if res, err := validation.IsValid(user); !res { + return nil, err + } + } + + return users, err +} + +func CountFederatedUsersByHostID(ctx context.Context, federationHostID int64) (int64, error) { + return db.GetEngine(ctx).Where("federation_host_id = ?", federationHostID).Count(FederatedUser{}) +} + +func FindFederatedUsersByHostID(ctx context.Context, federationHostID int64, opts db.ListOptions) ([]*FederatedUser, error) { + var users []*FederatedUser + sess := db.GetEngine(ctx).Where("federation_host_id = ?", federationHostID) + + if opts.PageSize > 0 { + sess = db.SetSessionPagination(sess, &opts) + } + + err := sess.Find(&users) + if err != nil { + return nil, err + } + + for _, user := range users { + if res, err := validation.IsValid(user); !res { + return nil, err + } + } + + return users, nil +} + func GetFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) { user, federatedUser, err := FindFederatedUser(ctx, externalID, federationHostID) if err != nil { diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 61378e0596..abe902dfc6 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -55,10 +55,12 @@ var UI = struct { } `ini:"ui.csv"` Admin struct { - UserPagingNum int - RepoPagingNum int - NoticePagingNum int - OrgPagingNum int + UserPagingNum int + RepoPagingNum int + NoticePagingNum int + OrgPagingNum int + FederationHostPagingNum int + FederationUserPagingNum int } `ini:"ui.admin"` User struct { RepoPagingNum int @@ -114,15 +116,19 @@ var UI = struct { MaxRows: 2500, }, Admin: struct { - UserPagingNum int - RepoPagingNum int - NoticePagingNum int - OrgPagingNum int + UserPagingNum int + RepoPagingNum int + NoticePagingNum int + OrgPagingNum int + FederationHostPagingNum int + FederationUserPagingNum int }{ - UserPagingNum: 50, - RepoPagingNum: 50, - NoticePagingNum: 25, - OrgPagingNum: 50, + UserPagingNum: 50, + RepoPagingNum: 50, + NoticePagingNum: 25, + OrgPagingNum: 50, + FederationHostPagingNum: 50, + FederationUserPagingNum: 50, }, User: struct { RepoPagingNum int diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json index 09b5d3fd69..aab1be7e1b 100644 --- a/options/locale_next/locale_en-US.json +++ b/options/locale_next/locale_en-US.json @@ -144,6 +144,37 @@ "keys.ssh.link": "SSH keys", "keys.gpg.link": "GPG keys", "keys.verify.token.hint": "The token is only valid for 1 minute. Get a new one if it expired.", + "admin.federation.federation": "Federation", + "admin.federation.hosts": "Hosts", + "admin.federation.hosts.title": "Federation hosts", + "admin.federation.hosts.manage_panel": "Manage federation hosts", + "admin.federation.hosts.details_panel": "Federation host details", + "admin.federation.hosts.show_details": "Show host details", + "admin.federation.host.id": "ID", + "admin.federation.host.fqdn": "FQDN", + "admin.federation.host.schema": "Schema", + "admin.federation.host.port": "Port", + "admin.federation.host.software_name": "Software", + "admin.federation.host.created": "Created", + "admin.federation.host.updated": "Updated", + "admin.federation.host.latest_activity": "Latest activity", + "admin.federation.users": "Users", + "admin.federation.users.title": "Federated users", + "admin.federation.users.manage_panel": "Manage federated users", + "admin.federation.users.show_local_user": "Show local user details", + "admin.federation.user.id": "ID", + "admin.federation.user.user_id": "Local user ID", + "admin.federation.user.external_id": "External user ID", + "admin.federation.user.inbox_path": "Inbox path", + "admin.config.federation": "Federation configuration", + "admin.config.federation.enabled": "Enabled", + "admin.config.federation.share_user_statistics": "Share user statistics with other hosts", + "admin.config.federation.max_size": "Max allowed response size", + "admin.config.federation.signature_enforced": "Require HTTP signatures", + "admin.config.federation.signature_algorithms": "Signature algorithms", + "admin.config.federation.digest_algorithm": "Signature digest algorithm", + "admin.config.federation.get_headers": "Signed GET headers", + "admin.config.federation.post_headers": "Signed POST headers", "admin.config.moderation_config": "Moderation configuration", "admin.moderation.moderation_reports": "Moderation reports", "admin.moderation.reports": "Reports", diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 2d3ea78052..c0eae0846b 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -148,6 +148,8 @@ func Config(ctx *context.Context) { ctx.Data["DbCfg"] = setting.Database ctx.Data["Webhook"] = setting.Webhook ctx.Data["Moderation"] = setting.Moderation + ctx.Data["Federation"] = setting.Federation + ctx.Data["FederationMaxSize"] = setting.Federation.MaxSize / 1024 / 1024 // in MiB ctx.Data["MailerEnabled"] = false if setting.MailService != nil { diff --git a/routers/web/admin/federation_host.go b/routers/web/admin/federation_host.go new file mode 100644 index 0000000000..278509f217 --- /dev/null +++ b/routers/web/admin/federation_host.go @@ -0,0 +1,65 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package admin + +import ( + "net/http" + + "forgejo.org/models/db" + "forgejo.org/models/forgefed" + user_model "forgejo.org/models/user" + "forgejo.org/modules/base" + "forgejo.org/modules/setting" + "forgejo.org/services/context" +) + +const ( + tplFederationHost base.TplName = "admin/federation/host" +) + +func FederationHost(ctx *context.Context) { + federationHostID := ctx.ParamsInt64("id") + page := ctx.FormInt("page") + if page < 1 { + page = 1 + } + + host, err := forgefed.GetFederationHost(ctx, federationHostID) + if err != nil { + ctx.ServerError("GetFederationHost", err) + return + } + + users, err := user_model.FindFederatedUsersByHostID(ctx, federationHostID, db.ListOptions{ + PageSize: setting.UI.Admin.FederationUserPagingNum, + Page: int(page), + }) + if err != nil { + ctx.ServerError("FindFederatedUsersByHostID", err) + return + } + + total, err := user_model.CountFederatedUsersByHostID(ctx, federationHostID) + if err != nil { + ctx.ServerError("CountFederatedUsersByHostID", err) + return + } + + ctx.Data["Host"] = host + ctx.Data["Users"] = users + ctx.Data["UsersTotal"] = int(total) + ctx.Data["Title"] = ctx.Tr("admin.federation.hosts.details_panel") + ctx.Data["PageIsAdminFederationHosts"] = true + + numPages := 0 + if total > 0 { + numPages = (int(total) - 1/setting.UI.Admin.FederationUserPagingNum) + } + + pager := context.NewPagination(int(total), setting.UI.Admin.FederationUserPagingNum, page, numPages) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplFederationHost) +} diff --git a/routers/web/admin/federation_hosts.go b/routers/web/admin/federation_hosts.go new file mode 100644 index 0000000000..e710f0aba5 --- /dev/null +++ b/routers/web/admin/federation_hosts.go @@ -0,0 +1,58 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package admin + +import ( + "net/http" + + "forgejo.org/models/db" + "forgejo.org/models/forgefed" + "forgejo.org/modules/base" + "forgejo.org/modules/setting" + "forgejo.org/services/context" +) + +const ( + tplFederationHosts base.TplName = "admin/federation/hosts" +) + +func FederationHosts(ctx *context.Context) { + sort := ctx.FormTrim("sort") + page := ctx.FormInt("page") + if page < 1 { + page = 1 + } + + hosts, err := forgefed.FindFederationHosts(ctx, db.ListOptions{ + Page: page, + PageSize: setting.UI.Admin.FederationHostPagingNum, + }) + if err != nil { + ctx.ServerError("GetFederationHosts", err) + return + } + + total, err := forgefed.CountFederationHosts(ctx) + if err != nil { + ctx.ServerError("CountFederationHosts", err) + return + } + + ctx.Data["Title"] = ctx.Tr("admin.federation.hosts.title") + ctx.Data["PageIsAdminFederationHosts"] = true + ctx.Data["SortType"] = sort + ctx.Data["TotalCount"] = int(total) + ctx.Data["Hosts"] = hosts + + numPages := 0 + if total > 0 { + numPages = (int(total) - 1/setting.UI.Admin.FederationHostPagingNum) + } + + pager := context.NewPagination(int(total), setting.UI.Admin.FederationHostPagingNum, page, numPages) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplFederationHosts) +} diff --git a/routers/web/admin/federation_users.go b/routers/web/admin/federation_users.go new file mode 100644 index 0000000000..098479dcc1 --- /dev/null +++ b/routers/web/admin/federation_users.go @@ -0,0 +1,56 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package admin + +import ( + "net/http" + + "forgejo.org/models/db" + user_model "forgejo.org/models/user" + "forgejo.org/modules/base" + "forgejo.org/modules/setting" + "forgejo.org/services/context" +) + +const ( + tplFederationUsers base.TplName = "admin/federation/users" +) + +func FederationUsers(ctx *context.Context) { + page := ctx.FormInt("page") + if page < 1 { + page = 1 + } + + users, err := user_model.FindFederatedUsers(ctx, db.ListOptions{ + Page: page, + PageSize: setting.UI.Admin.FederationUserPagingNum, + }) + if err != nil { + ctx.ServerError("FindFederatedUsers", err) + return + } + + total, err := user_model.CountFederatedUsers(ctx) + if err != nil { + ctx.ServerError("CountFederatedUsers", err) + return + } + + ctx.Data["Users"] = users + ctx.Data["TotalCount"] = int(total) + ctx.Data["Title"] = ctx.Tr("admin.federation.users.title") + ctx.Data["PageIsAdminFederationUsers"] = true + + numPages := 0 + if total > 0 { + numPages = (int(total) - 1/setting.UI.Admin.FederationUserPagingNum) + } + + pager := context.NewPagination(int(total), setting.UI.Admin.FederationUserPagingNum, page, numPages) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplFederationUsers) +} diff --git a/routers/web/web.go b/routers/web/web.go index 7463bf3ea2..766c39fa57 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -837,6 +837,14 @@ func registerRoutes(m *web.Route) { }) m.Post("/abuse_reports/act", admin.PerformAction) } + + if setting.Federation.Enabled { + m.Group("/federation", func() { + m.Get("/hosts", admin.FederationHosts) + m.Get("/users", admin.FederationUsers) + m.Get("/hosts/{id}", admin.FederationHost) + }) + } }, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "EnableModeration", setting.Moderation.Enabled)) // ***** END: Admin ***** diff --git a/services/context/context.go b/services/context/context.go index f0502b32fe..8ced686aac 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -184,6 +184,7 @@ func Contexter() func(next http.Handler) http.Handler { ctx.Data["DisableStars"] = setting.Repository.DisableStars ctx.Data["DisableForks"] = setting.Repository.DisableForks ctx.Data["EnableActions"] = setting.Actions.Enabled + ctx.Data["EnableFederation"] = setting.Federation.Enabled ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled() ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled() diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 0962c6bfc7..c4688922dd 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -344,6 +344,33 @@ +

+ {{ctx.Locale.Tr "admin.config.federation"}} +

+
+
+
{{ctx.Locale.Tr "admin.config.federation.enabled"}}
+
{{if .Federation.Enabled}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+
{{ctx.Locale.Tr "admin.config.federation.share_user_statistics"}}
+
{{if .Federation.ShareUserStatistics}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+
{{ctx.Locale.Tr "admin.config.federation.max_size"}}
+
{{.FederationMaxSize}} MiB
+ +
+ +
{{ctx.Locale.Tr "admin.config.federation.signature_enforced"}}
+
{{if .Federation.SignatureEnforced}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+
{{ctx.Locale.Tr "admin.config.federation.signature_algorithms"}}
+
{{.Federation.SignatureAlgorithms}}
+
{{ctx.Locale.Tr "admin.config.federation.digest_algorithm"}}
+
{{.Federation.DigestAlgorithm}}
+
{{ctx.Locale.Tr "admin.config.federation.get_headers"}}
+
{{.Federation.GetHeaders}}
+
{{ctx.Locale.Tr "admin.config.federation.post_headers"}}
+
{{.Federation.PostHeaders}}
+
+
+

{{ctx.Locale.Tr "admin.config.log_config"}}

diff --git a/templates/admin/federation/host.tmpl b/templates/admin/federation/host.tmpl new file mode 100644 index 0000000000..92fbd1ee2a --- /dev/null +++ b/templates/admin/federation/host.tmpl @@ -0,0 +1,39 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}} +
+ {{if .Host}} +

+ {{ctx.Locale.Tr "admin.federation.hosts.details_panel"}} +

+
+
+
{{ctx.Locale.Tr "admin.federation.host.id"}}
+
{{.Host.ID}}
+
{{ctx.Locale.Tr "admin.federation.host.fqdn"}}
+
{{.Host.HostFqdn}}
+
{{ctx.Locale.Tr "admin.federation.host.port"}}
+
{{.Host.HostPort}}
+
{{ctx.Locale.Tr "admin.federation.host.schema"}}
+
{{.Host.HostSchema}}
+
{{ctx.Locale.Tr "admin.federation.host.software_name"}}
+
{{.Host.NodeInfo.SoftwareName}}
+
{{ctx.Locale.Tr "admin.federation.host.created"}}
+
{{DateUtils.AbsoluteShort .Host.Created}}
+
{{ctx.Locale.Tr "admin.federation.host.updated"}}
+
{{DateUtils.AbsoluteShort .Host.Updated}}
+
{{ctx.Locale.Tr "admin.federation.host.latest_activity"}}
+
{{DateUtils.FullTime .Host.LatestActivity}}
+
+
+ {{end}} +

+ {{ctx.Locale.Tr "admin.federation.users.manage_panel"}} +
+ {{.UsersTotal}} +
+

+
+ {{template "admin/federation/user_list" .}} +
+ {{template "base/paginate" .}} +
+{{template "admin/layout_footer" .}} diff --git a/templates/admin/federation/hosts.tmpl b/templates/admin/federation/hosts.tmpl new file mode 100644 index 0000000000..2222873805 --- /dev/null +++ b/templates/admin/federation/hosts.tmpl @@ -0,0 +1,56 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}} +
+

+ {{ctx.Locale.Tr "admin.federation.hosts.manage_panel"}} ({{ctx.Locale.Tr "admin.total" .TotalCount}}) +

+
+ + + + + + + + + + + + + + + {{range .Hosts}} + + + + + + + + + + + {{else}} + + {{end}} + +
+ {{ctx.Locale.Tr "admin.federation.host.id"}} + {{SortArrow "id_asc" "id_desc" .SortType true}} + + {{ctx.Locale.Tr "admin.federation.host.fqdn"}} + {{SortArrow "fqdn_asc" "fqdn_desc" .SortType false}} + {{ctx.Locale.Tr "admin.federation.host.schema"}}{{ctx.Locale.Tr "admin.federation.host.port"}}{{ctx.Locale.Tr "admin.federation.host.software_name"}} + {{ctx.Locale.Tr "admin.federation.host.created"}} + {{SortArrow "created_asc" "created_desc" .SortType true}} + + {{ctx.Locale.Tr "admin.federation.host.latest_activity"}} + {{SortArrow "activity_asc" "activity_desc" .SortType true}} +
{{.ID}}{{.HostFqdn}}{{.HostSchema}}{{.HostPort}}{{.NodeInfo.SoftwareName}}{{DateUtils.AbsoluteShort .Created}}{{DateUtils.FullTime .LatestActivity}} + +
{{ctx.Locale.Tr "repo.pulls.no_results"}}
+
+ {{template "base/paginate" .}} +
+{{template "admin/layout_footer" .}} diff --git a/templates/admin/federation/user_list.tmpl b/templates/admin/federation/user_list.tmpl new file mode 100644 index 0000000000..18ff34b848 --- /dev/null +++ b/templates/admin/federation/user_list.tmpl @@ -0,0 +1,28 @@ + + + + + + + + + + + + {{range .Users}} + + + + + + + + {{else}} + + {{end}} + +
{{ctx.Locale.Tr "admin.federation.user.id"}}{{ctx.Locale.Tr "admin.federation.user.user_id"}}{{ctx.Locale.Tr "admin.federation.user.external_id"}}{{ctx.Locale.Tr "admin.federation.user.inbox_path"}}
{{.ID}}{{.UserID}}{{.ExternalID}}{{.InboxPath}} + +
{{ctx.Locale.Tr "repo.pulls.no_results"}}
diff --git a/templates/admin/federation/users.tmpl b/templates/admin/federation/users.tmpl new file mode 100644 index 0000000000..8ddcff3dad --- /dev/null +++ b/templates/admin/federation/users.tmpl @@ -0,0 +1,11 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}} +
+

+ {{ctx.Locale.Tr "admin.federation.users.manage_panel"}} ({{ctx.Locale.Tr "admin.total" .TotalCount}}) +

+
+ {{template "admin/federation/user_list" .}} +
+ {{template "base/paginate" .}} +
+{{template "admin/layout_footer" .}} diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 7ca8538bce..fa1500341d 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -77,6 +77,19 @@ {{end}} + {{if .EnableFederation}} +
+ {{ctx.Locale.Tr "admin.federation.federation"}} + +
+ {{end}}
{{ctx.Locale.Tr "admin.config"}}