mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-04 05:11:48 +02:00
models/user: add function to get FederatedUser by local userID
This commit is contained in:
parent
1af31f86da
commit
b29449c9c6
4 changed files with 181 additions and 17 deletions
|
@ -4,9 +4,11 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/modules/setting"
|
||||
)
|
||||
|
||||
|
@ -23,3 +25,15 @@ func (u *User) APActorID() string {
|
|||
func (u *User) APActorKeyID() string {
|
||||
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
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
|
|
@ -32,16 +32,24 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64) (
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func GetFederatedUserByKeyID(ctx context.Context, keyID *url.URL) (*FederatedUser, error) {
|
||||
user := new(FederatedUser)
|
||||
has, err := db.GetEngine(ctx).Where("key_id=?", keyID).Get(user)
|
||||
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
|
||||
}
|
||||
|
||||
return user, nil
|
||||
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 {
|
||||
|
|
|
@ -6,37 +6,130 @@ package activitypub
|
|||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/forgefed"
|
||||
"forgejo.org/models/user"
|
||||
"forgejo.org/modules/activitypub"
|
||||
fm "forgejo.org/modules/forgefed"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
gitea_context "forgejo.org/services/context"
|
||||
"forgejo.org/services/federation"
|
||||
|
||||
"github.com/42wim/httpsig"
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) {
|
||||
func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
|
||||
block, _ := pem.Decode([]byte(pubKeyPem))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
return nil, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||
}
|
||||
|
||||
return block.Bytes, nil
|
||||
}
|
||||
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, federatedUser, err := federation.CreateUserFromAP(ctx, personID, federationHost.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return federatedUser, nil
|
||||
}
|
||||
|
||||
func getPublicKeyFromResponse(ctx *gitea_context.APIContext, b []byte, keyID *url.URL) (p crypto.PublicKey, err error) {
|
||||
person := ap.PersonNew(ap.IRI(keyID.String()))
|
||||
err = person.UnmarshalJSON(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
|
||||
}
|
||||
|
||||
pubKey := person.PublicKey
|
||||
if pubKey.ID.String() != keyID.String() {
|
||||
return nil, fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b))
|
||||
}
|
||||
pubKeyPem := pubKey.PublicKeyPem
|
||||
block, _ := pem.Decode([]byte(pubKeyPem))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
return nil, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||
|
||||
pubKeyBytes, err := decodePublicKeyPem(pubKey.PublicKeyPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err = x509.ParsePKIXPublicKey(block.Bytes)
|
||||
|
||||
p, err = x509.ParsePKIXPublicKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache siging key in the database
|
||||
federationHost, err := federation.GetFederationHostForURI(ctx, person.ID.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if person.Type == ap.ActivityVocabularyType("Application") {
|
||||
federationHost.KeyID = sql.NullString{
|
||||
String: keyID.String(),
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
federationHost.PublicKey = sql.Null[sql.RawBytes]{
|
||||
V: pubKeyBytes,
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).ID(federationHost.ID).Update(federationHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if person.Type == ap.ActivityVocabularyType("Person") {
|
||||
federatedUser, err := getFederatedUser(ctx, person, federationHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
federatedUser.KeyID = sql.NullString{
|
||||
String: keyID.String(),
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
federatedUser.PublicKey = sql.Null[sql.RawBytes]{
|
||||
V: pubKeyBytes,
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).ID(federatedUser.ID).Update(federatedUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
||||
|
@ -59,9 +152,42 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er
|
|||
return false, err
|
||||
}
|
||||
|
||||
log.Debug("Fetching keyID: %s", ID)
|
||||
signatureAlgorithm := httpsig.Algorithm(setting.Federation.SignatureAlgorithms[0])
|
||||
|
||||
// 2. Fetch the public key of the other actor
|
||||
// Try if the signing actor is an already known federated user
|
||||
federationUser, err := user.GetFederatedUserByKeyID(ctx, idIRI.String())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if federationUser != nil && federationUser.PublicKey.Valid {
|
||||
pubKey, err := x509.ParsePKIXPublicKey(federationUser.PublicKey.V)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
|
||||
return authenticated, err
|
||||
}
|
||||
|
||||
// Try if the signing actor is an already known federation host
|
||||
federationHost, err := forgefed.FindFederationHostByKeyID(ctx, idIRI.String())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if federationHost != nil && federationHost.PublicKey.Valid {
|
||||
pubKey, err := x509.ParsePKIXPublicKey(federationHost.PublicKey.V)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
|
||||
return authenticated, err
|
||||
}
|
||||
|
||||
// Fetch missing public key
|
||||
actionsUser := user.NewAPActorUser()
|
||||
clientFactory, err := activitypub.GetClientFactory(ctx)
|
||||
if err != nil {
|
||||
|
@ -78,14 +204,12 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er
|
|||
return false, err
|
||||
}
|
||||
|
||||
pubKey, err := getPublicKeyFromResponse(b, idIRI)
|
||||
pubKey, err := getPublicKeyFromResponse(ctx, b, idIRI)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 3. Verify the other actor's key
|
||||
algo := httpsig.Algorithm(setting.Federation.SignatureAlgorithms[0])
|
||||
authenticated = v.Verify(pubKey, algo) == nil
|
||||
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
|
||||
return authenticated, err
|
||||
}
|
||||
|
||||
|
|
|
@ -10,8 +10,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/forgefed"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/models/user"
|
||||
"forgejo.org/modules/activitypub"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
|
@ -29,7 +30,7 @@ func TestFederationHttpSigValidation(t *testing.T) {
|
|||
userID := 2
|
||||
userURL := fmt.Sprintf("%vapi/v1/activitypub/user-id/%v", u, userID)
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user.User{ID: 1})
|
||||
|
||||
clientFactory, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||
require.NoError(t, err)
|
||||
|
@ -46,6 +47,23 @@ func TestFederationHttpSigValidation(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// HACK HACK HACK: the host part of the URL gets set to which IP forgejo is
|
||||
// listening on, NOT localhost, which is the Domain given to forgejo which
|
||||
// is then used for eg. the keyID all requests
|
||||
applicationKeyID := fmt.Sprintf("http://localhost:%v/api/v1/activitypub/actor#main-key", u.Port())
|
||||
actorKeyID := fmt.Sprintf("http://localhost:%v/api/v1/activitypub/user-id/1#main-key", u.Port())
|
||||
|
||||
// Check for cached public keys
|
||||
host, err := forgefed.FindFederationHostByKeyID(db.DefaultContext, applicationKeyID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, host)
|
||||
assert.True(t, host.PublicKey.Valid)
|
||||
|
||||
user, err := user.GetFederatedUserByKeyID(db.DefaultContext, actorKeyID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, user)
|
||||
assert.True(t, user.PublicKey.Valid)
|
||||
|
||||
// Disable signature validation
|
||||
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue