add port and schema to federation host (#7203)
## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [ ] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Co-authored-by: Michael Jerger <michael.jerger@meissa-gmbh.de> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7203 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: zam <mirco.zachmann@meissa.de> Co-committed-by: zam <mirco.zachmann@meissa.de>
This commit is contained in:
parent
23360ad415
commit
f6a5b783d2
20 changed files with 411 additions and 147 deletions
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package forgefed
|
package forgefed
|
||||||
|
@ -19,18 +19,22 @@ type FederationHost struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
|
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
|
||||||
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
|
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
|
||||||
|
HostPort uint16 `xorm:"NOT NULL DEFAULT 443"`
|
||||||
|
HostSchema string `xorm:"NOT NULL DEFAULT 'https'"`
|
||||||
LatestActivity time.Time `xorm:"NOT NULL"`
|
LatestActivity time.Time `xorm:"NOT NULL"`
|
||||||
Created timeutil.TimeStamp `xorm:"created"`
|
|
||||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
|
||||||
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
||||||
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
|
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Factory function for FederationHost. Created struct is asserted to be valid.
|
// Factory function for FederationHost. Created struct is asserted to be valid.
|
||||||
func NewFederationHost(nodeInfo NodeInfo, hostFqdn string) (FederationHost, error) {
|
func NewFederationHost(hostFqdn string, nodeInfo NodeInfo, port uint16, schema string) (FederationHost, error) {
|
||||||
result := FederationHost{
|
result := FederationHost{
|
||||||
HostFqdn: strings.ToLower(hostFqdn),
|
HostFqdn: strings.ToLower(hostFqdn),
|
||||||
NodeInfo: nodeInfo,
|
NodeInfo: nodeInfo,
|
||||||
|
HostPort: port,
|
||||||
|
HostSchema: schema,
|
||||||
}
|
}
|
||||||
if valid, err := validation.IsValid(result); !valid {
|
if valid, err := validation.IsValid(result); !valid {
|
||||||
return FederationHost{}, err
|
return FederationHost{}, err
|
||||||
|
@ -43,6 +47,8 @@ func (host FederationHost) Validate() []string {
|
||||||
var result []string
|
var result []string
|
||||||
result = append(result, validation.ValidateNotEmpty(host.HostFqdn, "HostFqdn")...)
|
result = append(result, validation.ValidateNotEmpty(host.HostFqdn, "HostFqdn")...)
|
||||||
result = append(result, validation.ValidateMaxLen(host.HostFqdn, 255, "HostFqdn")...)
|
result = append(result, validation.ValidateMaxLen(host.HostFqdn, 255, "HostFqdn")...)
|
||||||
|
result = append(result, validation.ValidateNotEmpty(host.HostPort, "HostPort")...)
|
||||||
|
result = append(result, validation.ValidateNotEmpty(host.HostSchema, "HostSchema")...)
|
||||||
result = append(result, host.NodeInfo.Validate()...)
|
result = append(result, host.NodeInfo.Validate()...)
|
||||||
if host.HostFqdn != strings.ToLower(host.HostFqdn) {
|
if host.HostFqdn != strings.ToLower(host.HostFqdn) {
|
||||||
result = append(result, fmt.Sprintf("HostFqdn has to be lower case but was: %v", host.HostFqdn))
|
result = append(result, fmt.Sprintf("HostFqdn has to be lower case but was: %v", host.HostFqdn))
|
||||||
|
|
|
@ -6,7 +6,6 @@ package forgefed
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
"forgejo.org/modules/validation"
|
"forgejo.org/modules/validation"
|
||||||
|
@ -44,8 +43,18 @@ func findFederationHostFromDB(ctx context.Context, searchKey, searchValue string
|
||||||
return host, nil
|
return host, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindFederationHostByFqdn(ctx context.Context, fqdn string) (*FederationHost, error) {
|
func FindFederationHostByFqdnAndPort(ctx context.Context, fqdn string, port uint16) (*FederationHost, error) {
|
||||||
return findFederationHostFromDB(ctx, "host_fqdn=?", strings.ToLower(fqdn))
|
host := new(FederationHost)
|
||||||
|
has, err := db.GetEngine(ctx).Where("host_fqdn=? AND host_port=?", fqdn, port).Get(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if res, err := validation.IsValid(host); !res {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return host, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindFederationHostByKeyID(ctx context.Context, keyID string) (*FederationHost, error) {
|
func FindFederationHostByKeyID(ctx context.Context, keyID string) (*FederationHost, error) {
|
||||||
|
|
|
@ -18,6 +18,8 @@ func Test_FederationHostValidation(t *testing.T) {
|
||||||
SoftwareName: "forgejo",
|
SoftwareName: "forgejo",
|
||||||
},
|
},
|
||||||
LatestActivity: time.Now(),
|
LatestActivity: time.Now(),
|
||||||
|
HostPort: 443,
|
||||||
|
HostSchema: "https",
|
||||||
}
|
}
|
||||||
if res, err := validation.IsValid(sut); !res {
|
if res, err := validation.IsValid(sut); !res {
|
||||||
t.Errorf("sut should be valid but was %q", err)
|
t.Errorf("sut should be valid but was %q", err)
|
||||||
|
@ -29,6 +31,8 @@ func Test_FederationHostValidation(t *testing.T) {
|
||||||
SoftwareName: "forgejo",
|
SoftwareName: "forgejo",
|
||||||
},
|
},
|
||||||
LatestActivity: time.Now(),
|
LatestActivity: time.Now(),
|
||||||
|
HostPort: 443,
|
||||||
|
HostSchema: "https",
|
||||||
}
|
}
|
||||||
if res, _ := validation.IsValid(sut); res {
|
if res, _ := validation.IsValid(sut); res {
|
||||||
t.Errorf("sut should be invalid: HostFqdn empty")
|
t.Errorf("sut should be invalid: HostFqdn empty")
|
||||||
|
@ -40,6 +44,8 @@ func Test_FederationHostValidation(t *testing.T) {
|
||||||
SoftwareName: "forgejo",
|
SoftwareName: "forgejo",
|
||||||
},
|
},
|
||||||
LatestActivity: time.Now(),
|
LatestActivity: time.Now(),
|
||||||
|
HostPort: 443,
|
||||||
|
HostSchema: "https",
|
||||||
}
|
}
|
||||||
if res, _ := validation.IsValid(sut); res {
|
if res, _ := validation.IsValid(sut); res {
|
||||||
t.Errorf("sut should be invalid: HostFqdn too long (len=256)")
|
t.Errorf("sut should be invalid: HostFqdn too long (len=256)")
|
||||||
|
@ -49,6 +55,8 @@ func Test_FederationHostValidation(t *testing.T) {
|
||||||
HostFqdn: "host.do.main",
|
HostFqdn: "host.do.main",
|
||||||
NodeInfo: NodeInfo{},
|
NodeInfo: NodeInfo{},
|
||||||
LatestActivity: time.Now(),
|
LatestActivity: time.Now(),
|
||||||
|
HostPort: 443,
|
||||||
|
HostSchema: "https",
|
||||||
}
|
}
|
||||||
if res, _ := validation.IsValid(sut); res {
|
if res, _ := validation.IsValid(sut); res {
|
||||||
t.Errorf("sut should be invalid: NodeInfo invalid")
|
t.Errorf("sut should be invalid: NodeInfo invalid")
|
||||||
|
@ -60,6 +68,8 @@ func Test_FederationHostValidation(t *testing.T) {
|
||||||
SoftwareName: "forgejo",
|
SoftwareName: "forgejo",
|
||||||
},
|
},
|
||||||
LatestActivity: time.Now().Add(1 * time.Hour),
|
LatestActivity: time.Now().Add(1 * time.Hour),
|
||||||
|
HostPort: 443,
|
||||||
|
HostSchema: "https",
|
||||||
}
|
}
|
||||||
if res, _ := validation.IsValid(sut); res {
|
if res, _ := validation.IsValid(sut); res {
|
||||||
t.Errorf("sut should be invalid: Future timestamp")
|
t.Errorf("sut should be invalid: Future timestamp")
|
||||||
|
@ -71,6 +81,8 @@ func Test_FederationHostValidation(t *testing.T) {
|
||||||
SoftwareName: "forgejo",
|
SoftwareName: "forgejo",
|
||||||
},
|
},
|
||||||
LatestActivity: time.Now(),
|
LatestActivity: time.Now(),
|
||||||
|
HostPort: 443,
|
||||||
|
HostSchema: "https",
|
||||||
}
|
}
|
||||||
if res, _ := validation.IsValid(sut); res {
|
if res, _ := validation.IsValid(sut); res {
|
||||||
t.Errorf("sut should be invalid: HostFqdn lower case")
|
t.Errorf("sut should be invalid: HostFqdn lower case")
|
||||||
|
|
|
@ -96,6 +96,8 @@ var migrations = []*Migration{
|
||||||
NewMigration("Add pronoun privacy settings to user", AddHidePronounsOptionToUser),
|
NewMigration("Add pronoun privacy settings to user", AddHidePronounsOptionToUser),
|
||||||
// v28 -> v29
|
// v28 -> v29
|
||||||
NewMigration("Add public key information to `FederatedUser` and `FederationHost`", AddPublicKeyInformationForFederation),
|
NewMigration("Add public key information to `FederatedUser` and `FederationHost`", AddPublicKeyInformationForFederation),
|
||||||
|
// v29 -> v30
|
||||||
|
NewMigration("Migrate `User.NormalizedFederatedURI` column to extract port & schema into FederatedHost", MigrateNormalizedFederatedURI),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
|
106
models/forgejo_migrations/v30.go
Normal file
106
models/forgejo_migrations/v30.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package forgejo_migrations //nolint:revive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forgejo.org/models/migrations/base"
|
||||||
|
"forgejo.org/modules/forgefed"
|
||||||
|
"forgejo.org/modules/log"
|
||||||
|
"forgejo.org/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MigrateNormalizedFederatedURI(x *xorm.Engine) error {
|
||||||
|
// Update schema
|
||||||
|
type FederatedUser struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
UserID int64 `xorm:"NOT NULL"`
|
||||||
|
ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
|
FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
|
NormalizedOriginalURL string
|
||||||
|
}
|
||||||
|
type User struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
NormalizedFederatedURI string
|
||||||
|
}
|
||||||
|
type FederationHost struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
|
||||||
|
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
|
||||||
|
HostPort uint16 `xorm:"NOT NULL DEFAULT 443"`
|
||||||
|
HostSchema string `xorm:"NOT NULL DEFAULT 'https'"`
|
||||||
|
LatestActivity time.Time `xorm:"NOT NULL"`
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
}
|
||||||
|
if err := x.Sync(new(User), new(FederatedUser), new(FederationHost)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate
|
||||||
|
sessMigration := x.NewSession()
|
||||||
|
defer sessMigration.Close()
|
||||||
|
if err := sessMigration.Begin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
federatedUsers := make([]*FederatedUser, 0)
|
||||||
|
err := sessMigration.OrderBy("id").Find(&federatedUsers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, federatedUser := range federatedUsers {
|
||||||
|
if federatedUser.NormalizedOriginalURL != "" {
|
||||||
|
log.Trace("migration[30]: FederatedUser was already migrated %v", federatedUser)
|
||||||
|
} else {
|
||||||
|
user := &User{}
|
||||||
|
has, err := sessMigration.Where("id=?", federatedUser.UserID).Get(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has {
|
||||||
|
log.Debug("migration[30]: User missing for federated user: %v", federatedUser)
|
||||||
|
_, err := sessMigration.Delete(federatedUser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Migrate User.NormalizedFederatedURI -> FederatedUser.NormalizedOriginalUrl
|
||||||
|
sql := "UPDATE `federated_user` SET `normalized_original_url` = ? WHERE `id` = ?"
|
||||||
|
if _, err := sessMigration.Exec(sql, user.NormalizedFederatedURI, federatedUser.FederationHostID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate (Port, Schema) FederatedUser.NormalizedOriginalUrl -> FederationHost.(Port, Schema)
|
||||||
|
actorID, err := forgefed.NewActorID(user.NormalizedFederatedURI)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sql = "UPDATE `federation_host` SET `host_port` = ?, `host_schema` = ? WHERE `id` = ?"
|
||||||
|
if _, err := sessMigration.Exec(sql, actorID.HostPort, actorID.HostSchema, federatedUser.FederationHostID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sessMigration.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop User.NormalizedFederatedURI field in extra transaction
|
||||||
|
sessSchema := x.NewSession()
|
||||||
|
defer sessSchema.Close()
|
||||||
|
if err := sessSchema.Begin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := base.DropTableColumns(sessSchema, "user", "normalized_federated_uri"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sessSchema.Commit()
|
||||||
|
}
|
81
models/forgejo_migrations/v30_test.go
Normal file
81
models/forgejo_migrations/v30_test.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package forgejo_migrations //nolint:revive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
migration_tests "forgejo.org/models/migrations/test"
|
||||||
|
"forgejo.org/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"xorm.io/xorm/schemas"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_MigrateNormalizedFederatedURI(t *testing.T) {
|
||||||
|
// Old structs
|
||||||
|
type User struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
NormalizedFederatedURI string
|
||||||
|
}
|
||||||
|
type FederatedUser struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
UserID int64 `xorm:"NOT NULL"`
|
||||||
|
ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
|
FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
|
}
|
||||||
|
type FederationHost struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
|
||||||
|
SoftwareName string `xorm:"NOT NULL"`
|
||||||
|
LatestActivity time.Time `xorm:"NOT NULL"`
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare TestEnv
|
||||||
|
x, deferable := migration_tests.PrepareTestEnv(t, 0,
|
||||||
|
new(User),
|
||||||
|
new(FederatedUser),
|
||||||
|
new(FederationHost),
|
||||||
|
)
|
||||||
|
defer deferable()
|
||||||
|
if x == nil || t.Failed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for expected results
|
||||||
|
getColumn := func(tn, co string) *schemas.Column {
|
||||||
|
tables, err := x.DBMetas()
|
||||||
|
require.NoError(t, err)
|
||||||
|
var table *schemas.Table
|
||||||
|
for _, elem := range tables {
|
||||||
|
if elem.Name == tn {
|
||||||
|
table = elem
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return table.GetColumn(co)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotNil(t, getColumn("user", "normalized_federated_uri"))
|
||||||
|
require.Nil(t, getColumn("federation_host", "host_port"))
|
||||||
|
require.Nil(t, getColumn("federation_host", "host_schema"))
|
||||||
|
cnt1, err := x.Table("federated_user").Count()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(2), cnt1)
|
||||||
|
|
||||||
|
require.NoError(t, MigrateNormalizedFederatedURI(x))
|
||||||
|
|
||||||
|
require.Nil(t, getColumn("user", "normalized_federated_uri"))
|
||||||
|
require.NotNil(t, getColumn("federation_host", "host_port"))
|
||||||
|
require.NotNil(t, getColumn("federation_host", "host_schema"))
|
||||||
|
cnt2, err := x.Table("federated_user").Count()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), cnt2)
|
||||||
|
|
||||||
|
// idempotent
|
||||||
|
require.NoError(t, MigrateNormalizedFederatedURI(x))
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
-
|
||||||
|
id: 1
|
||||||
|
user_id: 3
|
||||||
|
federation_host_id: 1
|
||||||
|
external_id: "18"
|
||||||
|
-
|
||||||
|
id: 2
|
||||||
|
user_id: 999
|
||||||
|
federation_host_id: 1
|
||||||
|
external_id: "19"
|
|
@ -0,0 +1,5 @@
|
||||||
|
-
|
||||||
|
id: 1
|
||||||
|
host_fqdn: "my.host.x"
|
||||||
|
software_name: forgejo
|
||||||
|
latest_activity: 2024-04-26 14:14:50
|
|
@ -0,0 +1,3 @@
|
||||||
|
-
|
||||||
|
id: 3
|
||||||
|
normalized_federated_uri: "https://my.host.x/api/activitypub/user-id/18"
|
|
@ -4,13 +4,10 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"forgejo.org/models/db"
|
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
"forgejo.org/modules/validation"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// APActorID returns the IRI to the api endpoint of the user
|
// APActorID returns the IRI to the api endpoint of the user
|
||||||
|
@ -26,19 +23,3 @@ func (u *User) APActorID() string {
|
||||||
func (u *User) APActorKeyID() string {
|
func (u *User) APActorKeyID() string {
|
||||||
return u.APActorID() + "#main-key"
|
return u.APActorID() + "#main-key"
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserByFederatedURI(ctx context.Context, federatedURI string) (*User, error) {
|
|
||||||
user := new(User)
|
|
||||||
has, err := db.GetEngine(ctx).Where("normalized_federated_uri=?", federatedURI).Get(user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !has {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if res, err := validation.IsValid(*user); !res {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"forgejo.org/models/db"
|
|
||||||
"forgejo.org/modules/validation"
|
"forgejo.org/modules/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FederatedUser struct {
|
type FederatedUser struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UserID int64 `xorm:"NOT NULL"`
|
UserID int64 `xorm:"NOT NULL"`
|
||||||
ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
||||||
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
|
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
|
||||||
|
NormalizedOriginalURL string // This field ist just to keep original information. Pls. do not use for search or as ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFederatedUser(userID int64, externalID string, federationHostID int64) (FederatedUser, error) {
|
func NewFederatedUser(userID int64, externalID string, federationHostID int64, normalizedOriginalURL string) (FederatedUser, error) {
|
||||||
result := FederatedUser{
|
result := FederatedUser{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ExternalID: externalID,
|
ExternalID: externalID,
|
||||||
FederationHostID: federationHostID,
|
FederationHostID: federationHostID,
|
||||||
|
NormalizedOriginalURL: normalizedOriginalURL,
|
||||||
}
|
}
|
||||||
if valid, err := validation.IsValid(result); !valid {
|
if valid, err := validation.IsValid(result); !valid {
|
||||||
return FederatedUser{}, err
|
return FederatedUser{}, err
|
||||||
|
@ -32,30 +32,6 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64) (
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFederatedUserFromDB(ctx context.Context, searchKey, searchValue any) (*FederatedUser, error) {
|
|
||||||
federatedUser := new(FederatedUser)
|
|
||||||
has, err := db.GetEngine(ctx).Where(searchKey, searchValue).Get(federatedUser)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !has {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if res, err := validation.IsValid(*federatedUser); !res {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return federatedUser, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFederatedUserByKeyID(ctx context.Context, keyID string) (*FederatedUser, error) {
|
|
||||||
return getFederatedUserFromDB(ctx, "key_id=?", keyID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFederatedUserByUserID(ctx context.Context, userID int64) (*FederatedUser, error) {
|
|
||||||
return getFederatedUserFromDB(ctx, "user_id=?", userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user FederatedUser) Validate() []string {
|
func (user FederatedUser) Validate() []string {
|
||||||
var result []string
|
var result []string
|
||||||
result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...)
|
result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
@ -135,9 +135,6 @@ type User struct {
|
||||||
AvatarEmail string `xorm:"NOT NULL"`
|
AvatarEmail string `xorm:"NOT NULL"`
|
||||||
UseCustomAvatar bool
|
UseCustomAvatar bool
|
||||||
|
|
||||||
// For federation
|
|
||||||
NormalizedFederatedURI string
|
|
||||||
|
|
||||||
// Counters
|
// Counters
|
||||||
NumFollowers int
|
NumFollowers int
|
||||||
NumFollowing int `xorm:"NOT NULL DEFAULT 0"`
|
NumFollowing int `xorm:"NOT NULL DEFAULT 0"`
|
||||||
|
|
|
@ -50,9 +50,7 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat
|
||||||
return committer.Commit()
|
return committer.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindFederatedUser(ctx context.Context, externalID string,
|
func FindFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) {
|
||||||
federationHostID int64,
|
|
||||||
) (*User, *FederatedUser, error) {
|
|
||||||
federatedUser := new(FederatedUser)
|
federatedUser := new(FederatedUser)
|
||||||
user := new(User)
|
user := new(User)
|
||||||
has, err := db.GetEngine(ctx).Where("external_id=? and federation_host_id=?", externalID, federationHostID).Get(federatedUser)
|
has, err := db.GetEngine(ctx).Where("external_id=? and federation_host_id=?", externalID, federationHostID).Get(federatedUser)
|
||||||
|
@ -77,6 +75,32 @@ func FindFederatedUser(ctx context.Context, externalID string,
|
||||||
return user, federatedUser, nil
|
return user, federatedUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *FederatedUser, error) {
|
||||||
|
federatedUser := new(FederatedUser)
|
||||||
|
user := new(User)
|
||||||
|
has, err := db.GetEngine(ctx).Where("key_id=?", keyID).Get(federatedUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, nil, fmt.Errorf("User %v for federated user is missing", federatedUser.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := validation.IsValid(*user); !res {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if res, err := validation.IsValid(*federatedUser); !res {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, federatedUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteFederatedUser(ctx context.Context, userID int64) error {
|
func DeleteFederatedUser(ctx context.Context, userID int64) error {
|
||||||
_, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID})
|
_, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID})
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
|
// Copyright 2023, 2024, 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package forgefed
|
package forgefed
|
||||||
|
@ -6,6 +6,7 @@ package forgefed
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"forgejo.org/modules/validation"
|
"forgejo.org/modules/validation"
|
||||||
|
@ -15,13 +16,14 @@ import (
|
||||||
|
|
||||||
// ----------------------------- ActorID --------------------------------------------
|
// ----------------------------- ActorID --------------------------------------------
|
||||||
type ActorID struct {
|
type ActorID struct {
|
||||||
ID string
|
ID string
|
||||||
Source string
|
Source string
|
||||||
Schema string
|
HostSchema string
|
||||||
Path string
|
Path string
|
||||||
Host string
|
Host string
|
||||||
Port string
|
HostPort uint16
|
||||||
UnvalidatedInput string
|
UnvalidatedInput string
|
||||||
|
IsPortSupplemented bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Factory function for ActorID. Created struct is asserted to be valid
|
// Factory function for ActorID. Created struct is asserted to be valid
|
||||||
|
@ -40,20 +42,23 @@ func NewActorID(uri string) (ActorID, error) {
|
||||||
|
|
||||||
func (id ActorID) AsURI() string {
|
func (id ActorID) AsURI() string {
|
||||||
var result string
|
var result string
|
||||||
if id.Port == "" {
|
|
||||||
result = fmt.Sprintf("%s://%s/%s/%s", id.Schema, id.Host, id.Path, id.ID)
|
if id.IsPortSupplemented {
|
||||||
|
result = fmt.Sprintf("%s://%s/%s/%s", id.HostSchema, id.Host, id.Path, id.ID)
|
||||||
} else {
|
} else {
|
||||||
result = fmt.Sprintf("%s://%s:%s/%s/%s", id.Schema, id.Host, id.Port, id.Path, id.ID)
|
result = fmt.Sprintf("%s://%s:%d/%s/%s", id.HostSchema, id.Host, id.HostPort, id.Path, id.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (id ActorID) Validate() []string {
|
func (id ActorID) Validate() []string {
|
||||||
var result []string
|
var result []string
|
||||||
result = append(result, validation.ValidateNotEmpty(id.ID, "userId")...)
|
result = append(result, validation.ValidateNotEmpty(id.ID, "userId")...)
|
||||||
result = append(result, validation.ValidateNotEmpty(id.Schema, "schema")...)
|
|
||||||
result = append(result, validation.ValidateNotEmpty(id.Path, "path")...)
|
result = append(result, validation.ValidateNotEmpty(id.Path, "path")...)
|
||||||
result = append(result, validation.ValidateNotEmpty(id.Host, "host")...)
|
result = append(result, validation.ValidateNotEmpty(id.Host, "host")...)
|
||||||
|
result = append(result, validation.ValidateNotEmpty(id.HostPort, "hostPort")...)
|
||||||
|
result = append(result, validation.ValidateNotEmpty(id.HostSchema, "hostSchema")...)
|
||||||
result = append(result, validation.ValidateNotEmpty(id.UnvalidatedInput, "unvalidatedInput")...)
|
result = append(result, validation.ValidateNotEmpty(id.UnvalidatedInput, "unvalidatedInput")...)
|
||||||
|
|
||||||
if id.UnvalidatedInput != id.AsURI() {
|
if id.UnvalidatedInput != id.AsURI() {
|
||||||
|
@ -104,12 +109,14 @@ func (id PersonID) Validate() []string {
|
||||||
result := id.ActorID.Validate()
|
result := id.ActorID.Validate()
|
||||||
result = append(result, validation.ValidateNotEmpty(id.Source, "source")...)
|
result = append(result, validation.ValidateNotEmpty(id.Source, "source")...)
|
||||||
result = append(result, validation.ValidateOneOf(id.Source, []any{"forgejo", "gitea"}, "Source")...)
|
result = append(result, validation.ValidateOneOf(id.Source, []any{"forgejo", "gitea"}, "Source")...)
|
||||||
|
|
||||||
switch id.Source {
|
switch id.Source {
|
||||||
case "forgejo", "gitea":
|
case "forgejo", "gitea":
|
||||||
if strings.ToLower(id.Path) != "api/v1/activitypub/user-id" && strings.ToLower(id.Path) != "api/activitypub/user-id" {
|
if strings.ToLower(id.Path) != "api/v1/activitypub/user-id" && strings.ToLower(id.Path) != "api/activitypub/user-id" {
|
||||||
result = append(result, fmt.Sprintf("path: %q has to be a person specific api path", id.Path))
|
result = append(result, fmt.Sprintf("path: %q has to be a person specific api path", id.Path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +175,8 @@ func removeEmptyStrings(ls []string) []string {
|
||||||
return rs
|
return rs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------- newActorID --------------------------------------------
|
||||||
|
|
||||||
func newActorID(uri string) (ActorID, error) {
|
func newActorID(uri string) (ActorID, error) {
|
||||||
validatedURI, err := url.ParseRequestURI(uri)
|
validatedURI, err := url.ParseRequestURI(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -179,15 +188,27 @@ func newActorID(uri string) (ActorID, error) {
|
||||||
}
|
}
|
||||||
length := len(pathWithActorID)
|
length := len(pathWithActorID)
|
||||||
pathWithoutActorID := strings.Join(pathWithActorID[0:length-1], "/")
|
pathWithoutActorID := strings.Join(pathWithActorID[0:length-1], "/")
|
||||||
id := pathWithActorID[length-1]
|
id := strings.ToLower(pathWithActorID[length-1])
|
||||||
|
|
||||||
result := ActorID{}
|
result := ActorID{}
|
||||||
result.ID = id
|
result.ID = id
|
||||||
result.Schema = validatedURI.Scheme
|
result.HostSchema = strings.ToLower(validatedURI.Scheme)
|
||||||
result.Host = validatedURI.Hostname()
|
result.Host = strings.ToLower(validatedURI.Hostname())
|
||||||
result.Path = pathWithoutActorID
|
result.Path = strings.ToLower(pathWithoutActorID)
|
||||||
result.Port = validatedURI.Port()
|
|
||||||
result.UnvalidatedInput = uri
|
if validatedURI.Port() == "" && result.HostSchema == "https" {
|
||||||
|
result.IsPortSupplemented = true
|
||||||
|
result.HostPort = 443
|
||||||
|
} else if validatedURI.Port() == "" && result.HostSchema == "http" {
|
||||||
|
result.IsPortSupplemented = true
|
||||||
|
result.HostPort = 80
|
||||||
|
} else {
|
||||||
|
numPort, _ := strconv.ParseUint(validatedURI.Port(), 10, 16)
|
||||||
|
result.HostPort = uint16(numPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.UnvalidatedInput = strings.ToLower(uri)
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
|
// Copyright 2023, 2024, 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package forgefed
|
package forgefed
|
||||||
|
@ -18,11 +18,13 @@ func TestNewPersonId(t *testing.T) {
|
||||||
expected := PersonID{}
|
expected := PersonID{}
|
||||||
expected.ID = "1"
|
expected.ID = "1"
|
||||||
expected.Source = "forgejo"
|
expected.Source = "forgejo"
|
||||||
expected.Schema = "https"
|
expected.HostSchema = "https"
|
||||||
expected.Path = "api/v1/activitypub/user-id"
|
expected.Path = "api/v1/activitypub/user-id"
|
||||||
expected.Host = "an.other.host"
|
expected.Host = "an.other.host"
|
||||||
expected.Port = ""
|
expected.HostPort = 443
|
||||||
|
expected.IsPortSupplemented = true
|
||||||
expected.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1"
|
expected.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1"
|
||||||
|
|
||||||
sut, _ := NewPersonID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo")
|
sut, _ := NewPersonID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo")
|
||||||
if sut != expected {
|
if sut != expected {
|
||||||
t.Errorf("expected: %v\n but was: %v\n", expected, sut)
|
t.Errorf("expected: %v\n but was: %v\n", expected, sut)
|
||||||
|
@ -31,15 +33,47 @@ func TestNewPersonId(t *testing.T) {
|
||||||
expected = PersonID{}
|
expected = PersonID{}
|
||||||
expected.ID = "1"
|
expected.ID = "1"
|
||||||
expected.Source = "forgejo"
|
expected.Source = "forgejo"
|
||||||
expected.Schema = "https"
|
expected.HostSchema = "https"
|
||||||
expected.Path = "api/v1/activitypub/user-id"
|
expected.Path = "api/v1/activitypub/user-id"
|
||||||
expected.Host = "an.other.host"
|
expected.Host = "an.other.host"
|
||||||
expected.Port = "443"
|
expected.HostPort = 443
|
||||||
|
expected.IsPortSupplemented = false
|
||||||
expected.UnvalidatedInput = "https://an.other.host:443/api/v1/activitypub/user-id/1"
|
expected.UnvalidatedInput = "https://an.other.host:443/api/v1/activitypub/user-id/1"
|
||||||
|
|
||||||
sut, _ = NewPersonID("https://an.other.host:443/api/v1/activitypub/user-id/1", "forgejo")
|
sut, _ = NewPersonID("https://an.other.host:443/api/v1/activitypub/user-id/1", "forgejo")
|
||||||
if sut != expected {
|
if sut != expected {
|
||||||
t.Errorf("expected: %v\n but was: %v\n", expected, sut)
|
t.Errorf("expected: %v\n but was: %v\n", expected, sut)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expected = PersonID{}
|
||||||
|
expected.ID = "1"
|
||||||
|
expected.Source = "forgejo"
|
||||||
|
expected.HostSchema = "http"
|
||||||
|
expected.Path = "api/v1/activitypub/user-id"
|
||||||
|
expected.Host = "an.other.host"
|
||||||
|
expected.HostPort = 80
|
||||||
|
expected.IsPortSupplemented = false
|
||||||
|
expected.UnvalidatedInput = "http://an.other.host:80/api/v1/activitypub/user-id/1"
|
||||||
|
|
||||||
|
sut, _ = NewPersonID("http://an.other.host:80/api/v1/activitypub/user-id/1", "forgejo")
|
||||||
|
if sut != expected {
|
||||||
|
t.Errorf("expected: %v\n but was: %v\n", expected, sut)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = PersonID{}
|
||||||
|
expected.ID = "1"
|
||||||
|
expected.Source = "forgejo"
|
||||||
|
expected.HostSchema = "https"
|
||||||
|
expected.Path = "api/v1/activitypub/user-id"
|
||||||
|
expected.Host = "an.other.host"
|
||||||
|
expected.HostPort = 443
|
||||||
|
expected.IsPortSupplemented = false
|
||||||
|
expected.UnvalidatedInput = "https://an.other.host:443/api/v1/activitypub/user-id/1"
|
||||||
|
|
||||||
|
sut, _ = NewPersonID("HTTPS://an.other.host:443/api/v1/activitypub/user-id/1", "forgejo")
|
||||||
|
if sut != expected {
|
||||||
|
t.Errorf("expected: %v\n but was: %v\n", expected, sut)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewRepositoryId(t *testing.T) {
|
func TestNewRepositoryId(t *testing.T) {
|
||||||
|
@ -47,10 +81,11 @@ func TestNewRepositoryId(t *testing.T) {
|
||||||
expected := RepositoryID{}
|
expected := RepositoryID{}
|
||||||
expected.ID = "1"
|
expected.ID = "1"
|
||||||
expected.Source = "forgejo"
|
expected.Source = "forgejo"
|
||||||
expected.Schema = "http"
|
expected.HostSchema = "http"
|
||||||
expected.Path = "api/activitypub/repository-id"
|
expected.Path = "api/activitypub/repository-id"
|
||||||
expected.Host = "localhost"
|
expected.Host = "localhost"
|
||||||
expected.Port = "3000"
|
expected.HostPort = 3000
|
||||||
|
expected.IsPortSupplemented = false
|
||||||
expected.UnvalidatedInput = "http://localhost:3000/api/activitypub/repository-id/1"
|
expected.UnvalidatedInput = "http://localhost:3000/api/activitypub/repository-id/1"
|
||||||
sut, _ := NewRepositoryID("http://localhost:3000/api/activitypub/repository-id/1", "forgejo")
|
sut, _ := NewRepositoryID("http://localhost:3000/api/activitypub/repository-id/1", "forgejo")
|
||||||
if sut != expected {
|
if sut != expected {
|
||||||
|
@ -61,10 +96,11 @@ func TestNewRepositoryId(t *testing.T) {
|
||||||
func TestActorIdValidation(t *testing.T) {
|
func TestActorIdValidation(t *testing.T) {
|
||||||
sut := ActorID{}
|
sut := ActorID{}
|
||||||
sut.Source = "forgejo"
|
sut.Source = "forgejo"
|
||||||
sut.Schema = "https"
|
sut.HostSchema = "https"
|
||||||
sut.Path = "api/v1/activitypub/user-id"
|
sut.Path = "api/v1/activitypub/user-id"
|
||||||
sut.Host = "an.other.host"
|
sut.Host = "an.other.host"
|
||||||
sut.Port = ""
|
sut.HostPort = 443
|
||||||
|
sut.IsPortSupplemented = true
|
||||||
sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/"
|
sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/"
|
||||||
if sut.Validate()[0] != "userId should not be empty" {
|
if sut.Validate()[0] != "userId should not be empty" {
|
||||||
t.Errorf("validation error expected but was: %v\n", sut.Validate())
|
t.Errorf("validation error expected but was: %v\n", sut.Validate())
|
||||||
|
@ -73,10 +109,11 @@ func TestActorIdValidation(t *testing.T) {
|
||||||
sut = ActorID{}
|
sut = ActorID{}
|
||||||
sut.ID = "1"
|
sut.ID = "1"
|
||||||
sut.Source = "forgejo"
|
sut.Source = "forgejo"
|
||||||
sut.Schema = "https"
|
sut.HostSchema = "https"
|
||||||
sut.Path = "api/v1/activitypub/user-id"
|
sut.Path = "api/v1/activitypub/user-id"
|
||||||
sut.Host = "an.other.host"
|
sut.Host = "an.other.host"
|
||||||
sut.Port = ""
|
sut.HostPort = 443
|
||||||
|
sut.IsPortSupplemented = true
|
||||||
sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1?illegal=action"
|
sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1?illegal=action"
|
||||||
if sut.Validate()[0] != "not all input was parsed, \nUnvalidated Input:\"https://an.other.host/api/v1/activitypub/user-id/1?illegal=action\" \nParsed URI: \"https://an.other.host/api/v1/activitypub/user-id/1\"" {
|
if sut.Validate()[0] != "not all input was parsed, \nUnvalidated Input:\"https://an.other.host/api/v1/activitypub/user-id/1?illegal=action\" \nParsed URI: \"https://an.other.host/api/v1/activitypub/user-id/1\"" {
|
||||||
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
|
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
|
||||||
|
@ -87,10 +124,11 @@ func TestPersonIdValidation(t *testing.T) {
|
||||||
sut := PersonID{}
|
sut := PersonID{}
|
||||||
sut.ID = "1"
|
sut.ID = "1"
|
||||||
sut.Source = "forgejo"
|
sut.Source = "forgejo"
|
||||||
sut.Schema = "https"
|
sut.HostSchema = "https"
|
||||||
sut.Path = "path"
|
sut.Path = "path"
|
||||||
sut.Host = "an.other.host"
|
sut.Host = "an.other.host"
|
||||||
sut.Port = ""
|
sut.HostPort = 443
|
||||||
|
sut.IsPortSupplemented = true
|
||||||
sut.UnvalidatedInput = "https://an.other.host/path/1"
|
sut.UnvalidatedInput = "https://an.other.host/path/1"
|
||||||
|
|
||||||
_, err := validation.IsValid(sut)
|
_, err := validation.IsValid(sut)
|
||||||
|
@ -101,10 +139,11 @@ func TestPersonIdValidation(t *testing.T) {
|
||||||
sut = PersonID{}
|
sut = PersonID{}
|
||||||
sut.ID = "1"
|
sut.ID = "1"
|
||||||
sut.Source = "forgejox"
|
sut.Source = "forgejox"
|
||||||
sut.Schema = "https"
|
sut.HostSchema = "https"
|
||||||
sut.Path = "api/v1/activitypub/user-id"
|
sut.Path = "api/v1/activitypub/user-id"
|
||||||
sut.Host = "an.other.host"
|
sut.Host = "an.other.host"
|
||||||
sut.Port = ""
|
sut.HostPort = 443
|
||||||
|
sut.IsPortSupplemented = true
|
||||||
sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1"
|
sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1"
|
||||||
if sut.Validate()[0] != "Value forgejox is not contained in allowed values [forgejo gitea]" {
|
if sut.Validate()[0] != "Value forgejox is not contained in allowed values [forgejo gitea]" {
|
||||||
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
|
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
|
||||||
|
@ -125,12 +164,10 @@ func TestWebfingerId(t *testing.T) {
|
||||||
|
|
||||||
func TestShouldThrowErrorOnInvalidInput(t *testing.T) {
|
func TestShouldThrowErrorOnInvalidInput(t *testing.T) {
|
||||||
var err any
|
var err any
|
||||||
// TODO: remove after test
|
_, err = NewPersonID("", "forgejo")
|
||||||
//_, err = NewPersonId("", "forgejo")
|
if err == nil {
|
||||||
//if err == nil {
|
t.Errorf("empty input should be invalid.")
|
||||||
// t.Errorf("empty input should be invalid.")
|
}
|
||||||
//}
|
|
||||||
|
|
||||||
_, err = NewPersonID("http://localhost:3000/api/v1/something", "forgejo")
|
_, err = NewPersonID("http://localhost:3000/api/v1/something", "forgejo")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("localhost uris are not external")
|
t.Errorf("localhost uris are not external")
|
||||||
|
@ -155,7 +192,6 @@ func TestShouldThrowErrorOnInvalidInput(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("uri may not contain unparsed elements")
|
t.Errorf("uri may not contain unparsed elements")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = NewPersonID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo")
|
_, err = NewPersonID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("this uri should be valid but was: %v", err)
|
t.Errorf("this uri should be valid but was: %v", err)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
// Copyright 2023, 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package forgefed
|
package forgefed
|
||||||
|
@ -10,10 +10,10 @@ import (
|
||||||
func (id ActorID) AsWellKnownNodeInfoURI() string {
|
func (id ActorID) AsWellKnownNodeInfoURI() string {
|
||||||
wellKnownPath := ".well-known/nodeinfo"
|
wellKnownPath := ".well-known/nodeinfo"
|
||||||
var result string
|
var result string
|
||||||
if id.Port == "" {
|
if id.HostPort == 0 {
|
||||||
result = fmt.Sprintf("%s://%s/%s", id.Schema, id.Host, wellKnownPath)
|
result = fmt.Sprintf("%s://%s/%s", id.HostSchema, id.Host, wellKnownPath)
|
||||||
} else {
|
} else {
|
||||||
result = fmt.Sprintf("%s://%s:%s/%s", id.Schema, id.Host, id.Port, wellKnownPath)
|
result = fmt.Sprintf("%s://%s:%d/%s", id.HostSchema, id.Host, id.HostPort, wellKnownPath)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
// Copyright 2023, 2024, 2025 The Forgejo Authors. All rights reserved.
|
||||||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package validation
|
package validation
|
||||||
|
@ -33,9 +32,9 @@ type Validateable interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsValid(v Validateable) (bool, error) {
|
func IsValid(v Validateable) (bool, error) {
|
||||||
if err := v.Validate(); len(err) > 0 {
|
if valdationErrors := v.Validate(); len(valdationErrors) > 0 {
|
||||||
typeof := reflect.TypeOf(v)
|
typeof := reflect.TypeOf(v)
|
||||||
errString := strings.Join(err, "\n")
|
errString := strings.Join(valdationErrors, "\n")
|
||||||
return false, ErrNotValid{fmt.Sprint(typeof, ": ", errString)}
|
return false, ErrNotValid{fmt.Sprint(typeof, ": ", errString)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +52,10 @@ func ValidateNotEmpty(value any, name string) []string {
|
||||||
if v.IsZero() {
|
if v.IsZero() {
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
|
case uint16:
|
||||||
|
if v == 0 {
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
case int64:
|
case int64:
|
||||||
if v == 0 {
|
if v == 0 {
|
||||||
isValid = false
|
isValid = false
|
||||||
|
|
|
@ -36,33 +36,25 @@ func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFederatedUser(ctx *gitea_context.APIContext, person *ap.Person, federationHost *forgefed.FederationHost) (*user.FederatedUser, error) {
|
func getFederatedUser(ctx *gitea_context.APIContext, person *ap.Person, federationHost *forgefed.FederationHost) (*user.FederatedUser, error) {
|
||||||
dbUser, err := user.GetUserByFederatedURI(ctx, person.ID.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if dbUser != nil {
|
|
||||||
federatedUser, err := user.GetFederatedUserByUserID(ctx, dbUser.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if federatedUser != nil {
|
|
||||||
return federatedUser, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
personID, err := fm.NewPersonID(person.ID.String(), string(federationHost.NodeInfo.SoftwareName))
|
personID, err := fm.NewPersonID(person.ID.String(), string(federationHost.NodeInfo.SoftwareName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
_, federatedUser, err := user.FindFederatedUser(ctx, personID.ID, federationHost.ID)
|
||||||
_, federatedUser, err := federation.CreateUserFromAP(ctx, personID, federationHost.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return federatedUser, nil
|
if federatedUser != nil {
|
||||||
|
return federatedUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, newFederatedUser, err := federation.CreateUserFromAP(ctx, personID, federationHost.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFederatedUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func storePublicKey(ctx *gitea_context.APIContext, person *ap.Person, pubKeyBytes []byte) error {
|
func storePublicKey(ctx *gitea_context.APIContext, person *ap.Person, pubKeyBytes []byte) error {
|
||||||
|
@ -159,7 +151,7 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er
|
||||||
|
|
||||||
// 2. Fetch the public key of the other actor
|
// 2. Fetch the public key of the other actor
|
||||||
// Try if the signing actor is an already known federated user
|
// Try if the signing actor is an already known federated user
|
||||||
federationUser, err := user.GetFederatedUserByKeyID(ctx, idIRI.String())
|
_, federationUser, err := user.FindFederatedUserByKeyID(ctx, idIRI.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package federation
|
package federation
|
||||||
|
@ -129,7 +129,7 @@ func CreateFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forge
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := forgefed.NewFederationHost(nodeInfo, actorID.Host)
|
result, err := forgefed.NewFederationHost(actorID.Host, nodeInfo, actorID.HostPort, actorID.HostSchema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -148,7 +148,7 @@ func GetFederationHostForURI(ctx context.Context, actorURI string) (*forgefed.Fe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
federationHost, err := forgefed.FindFederationHostByFqdn(ctx, rawActorID.Host)
|
federationHost, err := forgefed.FindFederationHostByFqdnAndPort(ctx, rawActorID.Host, rawActorID.HostPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -221,12 +221,12 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
|
||||||
LoginName: loginName,
|
LoginName: loginName,
|
||||||
Type: user.UserTypeRemoteUser,
|
Type: user.UserTypeRemoteUser,
|
||||||
IsAdmin: false,
|
IsAdmin: false,
|
||||||
NormalizedFederatedURI: personID.AsURI(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
federatedUser := user.FederatedUser{
|
federatedUser := user.FederatedUser{
|
||||||
ExternalID: personID.ID,
|
ExternalID: personID.ID,
|
||||||
FederationHostID: federationHostID,
|
FederationHostID: federationHostID,
|
||||||
|
NormalizedOriginalURL: personID.AsURI(),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = user.CreateFederatedUser(ctx, &newUser, &federatedUser)
|
err = user.CreateFederatedUser(ctx, &newUser, &federatedUser)
|
||||||
|
|
|
@ -64,7 +64,7 @@ func TestFederationHttpSigValidation(t *testing.T) {
|
||||||
assert.NotNil(t, host)
|
assert.NotNil(t, host)
|
||||||
assert.True(t, host.PublicKey.Valid)
|
assert.True(t, host.PublicKey.Valid)
|
||||||
|
|
||||||
user, err := user.GetFederatedUserByKeyID(db.DefaultContext, actorKeyID)
|
_, user, err := user.FindFederatedUserByKeyID(db.DefaultContext, actorKeyID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, user)
|
assert.NotNil(t, user)
|
||||||
assert.True(t, user.PublicKey.Valid)
|
assert.True(t, user.PublicKey.Valid)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue