4.4 KiB
4.4 KiB
Federation Architecture Principles
While implementing federation in forgejo we introduced some conncepts from DomainDrivenDesign:
- Aggregate: Aggregates are clusters of objects (entities or values) which are handled atomic when it comes to persistence.
- Validation: Every object should express it's own validity, whenever someone is interested in
- we collect as many invalidity information as possible in one shoot - so we return a list of validation issues if there are some.
- Objects entering the lifetime are checked for validity on the borders (after loaded from and before stored to DB, after being newly created (New* functions) or after loaded via web / REST).
Objects in forgefed package reflect Objects from ap or f3 lib but add some Forgejo specific enhancements like more specific validation.
Federation Model
classDiagram
namespace activitypub {
class Activity {
ID ID
Type ActivityVocabularyType // Like
Actor Item
Object Item
}
class Actor {
ID
Type ActivityVocabularyType // Person
Name NaturalLanguageValues
PreferredUsername NaturalLanguageValues
Inbox Item
Outbox Item
PublicKey PublicKey
}
}
namespace forgfed {
class ForgePerson {
Validate() []string
}
class ForgeLike {
Actor PersonID
Validate() []string
}
class ActorID {
ID string
Schema string
Path string
Host string
Port string
Source string
UnvalidatedInput string
Validate() []string
}
class PersonID {
AsLoginName() string // "ID-Host"
AsWebfinger() string // "@ID@Host"
Validate() []string
}
class RepositoryID {
Validate() []string
}
class FederationHost {
<<Aggregate Root>>
ID int64
HostFqdn string
Validate() []string
}
class NodeInfo {
Source string
Validate() []string
}
}
Actor <|-- ForgePerson
Activity <|-- ForgeLike
ActorID <|-- PersonID
ActorID <|-- RepositoryID
ForgeLike *-- PersonID: Actor
ForgePerson -- PersonID: links to
FederationHost *-- NodeInfo
namespace user {
class User {
<<Aggregate Root>>
ID int64
LowerName string
Name string
Email string
Passwd string
LoginName string
Type UserType
IsActive bool
IsAdmin bool
NormalizedFederatedURI string
Validate() []string
}
class FederatedUser {
ID int64
UserID int64
ExternalID string
FederationHost int64
Validate() []string
}
}
namespace repository {
class Repository {
<<Aggregate Root>>
ID int64
}
class FollowingRepository {
ID int64
RepositoryID int64
ExternalID string
FederationHost int64
Validate() []string
}
}
User "1" *-- "1" FederatedUser: FederatedUser.UserID
PersonID -- FederatedUser : mapped by PersonID.ID == FederatedUser.externalID & FederationHost.ID
PersonID -- FederationHost : mapped by PersonID.Host == FederationHost.HostFqdn
FederatedUser -- FederationHost
Repository "1" *-- "n" FollowingRepository: FollowingRepository.RepositoryID
FollowingRepository -- FederationHost
Normalized URI used as ID
In order to use URIs as ID we've to normalize URIs.
A normalized user URI looks like: https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/1
In order to normalize URIs we care:
- Case (all to lower case):
https://federated-REPO.prod.meissa.de/api/v1/activitypub/user-id/1
- No relative path:
https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/../user-id/1
- No parameters:
https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/1?some-parameters=1
- No Webfinger:
https://user1@federated-repo.prod.meissa.de
(with following redirects) - No default api:
https://federated-repo.prod.meissa.de/api/activitypub/user-id/1
- No autorization:
https://user:password@federated-repo.prod.meissa.de/api/v1/activitypub/user-id/1
- No default ports:
https://federated-repo.prod.meissa.de:443/api/v1/activitypub/user-id/1
- Accept non default ports:
http://localhost:3000/api/v1/activitypub/user-id/1