13 KiB
Activity for federated star action
Status
Active
Context
While implementing federation we have to represent federated persons on a local instance.
A federated person should be able to execute local actions (as if he was a local user), ideally without too many code changes.
For being able to map the federated person reliable, the local representation has to carry a clear mapping to the original federated person.
We get actor information as {"actor": "https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",}
. To find out whether this user is available locally without dereferencing the federated person every time is important for performance & system resilience.
Decision
We decided to use option 3 "Map to User & FederatedUser".
Discussion can be found here: https://codeberg.org/forgejo/discussions/issues/101
Choices
1. Map to plain forgejo User
- We map PersonId AsLoginName() (e.g. 13-some.instan.ce) to User.LoginName. Due to limitations of User.LoginName validation mapping may be affected by invalid characters.
- Created User is limited:
- non functional email is generated, email notification is false. At the moment we have problems with email whitelists at this point.
- strong password is generated silently
- User.Type is UserTypeRemoteUser
- User is not Admin
- User is not Active
1. Pro
- We can use forgejo code (like star / unstar fkt.) without changes.
- No new model & persistence is introduced, architectural change is small.
1. Con
- But we use fields against their semantic and see some problems / limitations for mapping arise.
- generating email having the source fqdn is impacted by email whitelists.
- loginName is used for mapping, but e.g. @ is not allowed.
- password is generated headless.
- Maybe the large User table gets even larger (see https://git.exozy.me/a/gitea/issues/2)
- Occasional contributors may not understand the difference in level of trust implied by federated user. This may promote errors with security impact.
- Understanding federated users entries being kind of cache would conflict with user table entries.
- LoginNames may be occupied by federated users. This may leak information and increase attack surface.
classDiagram
namespace activitypub {
class Like {
ID ID
Type ActivityVocabularyType // Like
Actor Item
Object Item
}
class Actor {
ID
URL Item
Type ActivityVocabularyType // Person
Name NaturalLanguageValues
PreferredUsername NaturalLanguageValues
Inbox Item
Outbox Item
PublicKey PublicKey
}
}
namespace forgfed {
class ForgePerson {
}
class ForgeLike {
Actor PersonID
}
class ActorID {
ID string
Schema string
Path string
Host string
Port string
UnvalidatedInput string
}
class PersonID {
AsLoginName() string // "ID-Host"
}
}
namespace forgejo {
class User {
<<Aggregate Root>>
ID int64
LowerName string
Name string
Email string
Passwd string
LoginName string
Type UserType
IsActive bool
IsAdmin bool
}
}
Actor <|-- ForgePerson
Like <|-- ForgeLike
ActorID <|-- PersonID
ForgeLike *-- PersonID: Actor
PersonID -- User: mapped by AsLoginName() == LoginName
PersonID -- ForgePerson: links to
2. Map to User-&-ExternalLoginUser
- We map PersonId.AsWebfinger() (e.g. 13@some.instan.ce) to ExternalLoginUser.ExternalID. LoginSourceID may be left Empty.
- Created User is limited:
- non functional email is generated, email notification is false.
- strong password is generated silently
- User.Type is UserTypeRemoteUser
- User is not Admin
- User is not Active
- Created ExternalLoginUser is limited
- Login via fediverse is not intended and will not work. This is distinct to the F3 usecase.
2. Pro
- We can use forgejo code (like star / unstar fkt.) without changes.
- No new model & persistence is introduced, architectural change is small. Comparable to option 1.
- This option was taken by the F3-Export/Import-Feature
- Mapping may be more reliable compared to option 1.
2. Con
- We use fields against their semantic (User.EMail, User.Password, User.LoginSource, ExternalLoginUser.Login*) and see some problems / limitations for login functionality arise. Situation is worse than option 1.
- generating email having the source fqdn is impacted by email whitelists.
- password is generated headless.
- TODO: How would we map/generate User.LoginName ?
- TODO: How would we generate ExternalLoginUser.Login* fields?
- Getting a larger User table applies to this solution comparable to option 1.
- Occasional contributors may not understand the difference in level of trust implied by federated user, this may promote errors with security impact.
- Understanding federated users entries being kind of cache would conflict with user table entries.
- LoginNames may be occupied by federated users. This may leak information and increase attack surface.
classDiagram
namespace activitypub {
class Like {
ID ID
Type ActivityVocabularyType // Like
Actor Item
Object Item
}
class Actor {
ID
URL Item
Type ActivityVocabularyType // Person
Name NaturalLanguageValues
PreferredUsername NaturalLanguageValues
Inbox Item
Outbox Item
PublicKey PublicKey
}
}
namespace forgfed {
class ForgePerson {
}
class ForgeLike {
Actor PersonID
}
class ActorID {
ID string
Schema string
Path string
Host string
Port string
UnvalidatedInput string
}
class PersonID {
AsWebfinger() string // "ID@Host"
}
}
namespace user {
class User {
<<Aggregate Root>>
ID int64
LoginSource int64
LowerName string
Name string
Email string
Passwd string
LoginName string
Type UserType
IsActive bool
IsAdmin bool
}
class ExternalLoginUser {
ExternalID string
LoginSourceID int64
RawData map[string]any
Provider string
}
}
namespace auth {
class Source {
<<Aggregate Root>>
ID int64
Type Type
Name string
IsActive bool
IsSyncEnabled bool
}
}
Actor <|-- ForgePerson
Like <|-- ForgeLike
ActorID <|-- PersonID
ForgeLike *-- PersonID: Actor
PersonID -- ForgePerson: links to
PersonID -- ExternalLoginUser: mapped by AsLoginName() == ExternalID
User *-- ExternalLoginUser: ExternalLoginUser.UserID
User -- Source
ExternalLoginUser -- Source
3. Map to User-&-FederatedUser
- We map PersonId.asWbfinger() to FederatedPerson.ExternalID (e.g. 13@some.instan.ce).
- Created User is limited:
- non functional email is generated, email notification is false.
- strong password is generated silently
- User.Type is UserTypeRemoteUser
- User is not Admin
- User is not Active
3. Pro
- We can use forgejo code (like star / unstar fkt.) without changes.
- Introduce FederatedUser as new model & persistence, architectural change is medium.
- We will be able to have a reliable mapping. Better than option 1 & 2.
3. Con
- But we use fields (User.EMail, User.Password) against their semantic, but we probably can handle the problems arising. Situation is comparable to option 1.
- generating email having the source fqdn is impacted by email whitelists.
- password is generated headless.
- TODO: How would we map/generate User.LoginName ?
- Getting a larger User table applies to this solution comparable to option 1.
- Occasional contributors may not understand the difference in level of trust implied by federated user, this may promote errors with security impact, comparable to option 1.
- Getting a larger User table applies to this solution comparable to option 1.
- Understanding federated users entries being kind of cache would conflict with user table entries.
- LoginNames may be occupied by federated users. This may leak information and increase attack surface.
classDiagram
namespace activitypub {
class Like {
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 {
}
class ForgeLike {
Actor PersonID
}
class ActorID {
ID string
Schema string
Path string
Host string
Port string
UnvalidatedInput string
}
class PersonID {
AsLoginName() string // "ID-Host"
AsWebfinger() string // "@ID@Host"
}
class FederationHost {
<<Aggregate Root>>
ID int64
HostFqdn string
}
class NodeInfo {
Source string
}
}
namespace user {
class User {
<<Aggregate Root>>
ID int64
LowerName string
Name string
Email string
Passwd string
LoginName string
Type UserType
IsActive bool
IsAdmin bool
}
class FederatedUser {
ID int64
UserID int64
ExternalID string
FederationHost int64
}
}
Actor <|-- ForgePerson
Like <|-- ForgeLike
ActorID <|-- PersonID
ForgeLike *-- PersonID: Actor
ForgePerson -- PersonID: links to
FederationHost *-- NodeInfo
User *-- FederatedUser: FederatedUser.UserID
PersonID -- FederatedUser : mapped by PersonID.asWebfinger() == FederatedUser.externalID
FederatedUser -- FederationHost
4. Map to new FederatedPerson and introduce a common User interface
- We map PersonId.asWbfinger() to FederatedPerson.ExternalID (e.g. 13@some.instan.ce).
- We will have no semantic mismatch.
4. Pro
- We will be able to have a reliable mapping.
- We will not use fields against their semantics.
- We do not enhance user table with "cache entries". Forgejo stays scalable, no additional DOS surface.
- Occasional contributors may understand a clear difference between user and federated user.
- No LoginNames where occupied
- Caching aspects of federated users (like refresh, evict) may be easier to implement.
4. Con
- We can use forgejo code (like star / unstar fkt.) after refactorings only.
- At every place of interaction we have to enhance persistence (e.g. a find may have to query two tables now) & introduce a common User interface.
- We introduce new model & persistence.
classDiagram
namespace activitypub {
class Like {
ID ID
Type ActivityVocabularyType // Like
Actor Item
Object Item
}
class Actor {
ID
URL Item
Type ActivityVocabularyType // Person
Name NaturalLanguageValues
PreferredUsername NaturalLanguageValues
Inbox Item
Outbox Item
PublicKey PublicKey
}
}
namespace forgfed {
class ForgePerson {
}
class ForgeLike {
Actor PersonID
}
class ActorID {
ID string
Schema string
Path string
Host string
Port string
UnvalidatedInput string
}
class PersonID {
AsLoginName() string // "ID-Host"
AsWebfinger() string // "@ID@Host"
}
class FederatedPerson {
<<Aggregate Root>>
ID int64
UserID int64
RawData map[string]any
ExternalID string
FederationHost int64
}
class FederationHost {
<<Aggregate Root>>
ID int64
HostFqdn string
}
class NodeInfo {
Source string
}
}
namespace user {
class CommonUser {
<<Interface>>
}
class User {
}
}
User ..<| CommonUser
Actor <|-- ForgePerson
Like <|-- ForgeLike
ActorID <|-- PersonID
ForgeLike *-- PersonID: Actor
PersonID -- ForgePerson: links to
PersonID -- FederatedPerson : mapped by PersonID.asWebfinger() == FederatedPerson.externalID
FederationHost *-- NodeInfo
FederatedPerson -- FederationHost
FederatedPerson ..<| CommonUser