forgejo/docs/unsure-where-to-put/adr-map-federated-person.md
2024-02-10 15:18:54 +01:00

9.7 KiB

Activity for federated star action

Status

Still in discussion

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 dereference the federated person every time is important for performance & system resilience.

Decision

tbd

Choices

1. Map to plain forgejo User

  1. 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.
  2. Created User is limited:
    1. non functional email is generated, email notification is false. At the moment we have problems with email whitelists at this point.
    2. strong password is generated silently
    3. User.Type is UserTypeRemoteUser
    4. User is not Admin
    5. User is not Active

We can use forgejo code (like star / unstar fkt.) without changes.

No new model & persistence is introduced.

But we use fields against their semantic and see some problems / limitations for mapping arise.

classDiagram
  namespace activitypub {
    class ForgeLike {
      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
    }
    class ActorID {
      ID               string
      Source           string
      Schema           string
      Path             string
      Host             string
      Port             string
      UnvalidatedInput string
    }
    class PersonID {
      AsLoginName() string // "ID-Host"
    }
  }

  ActorID <|-- PersonID
  ForgeLike *-- PersonID: ActorID

  namespace forgejo {
    class User {
      <<Aggragate Root>>
      ID        int64 
      LowerName string
      Name      string
      Email     string
      Passwd    string
      LoginName   string
      Type        UserType
      IsActive bool
      IsAdmin bool
    }
  }

  PersonID -- User: mapped by AsLoginName() == LoginName
  PersonID -- Actor: links to

2. Map to User-&-ExternalLoginUser

  1. We map PersonId.AsWebfinger() (e.g. 13@some.instan.ce) to ExternalLoginUser.ExternalID. LoginSourceID may be left Empty.
  2. Created User is limited:
    1. non functional email is generated, email notification is false.
    2. strong password is generated silently
    3. User.Type is UserTypeRemoteUser
    4. User is not Admin
    5. User is not Active
  3. Created ExternalLoginUser is limited
    1. Login via fediverse is not intended and will not work

We can use forgejo code (like star / unstar fkt.) without changes.

No new model & persistence is introduced, no need for refactorings.

But we use fields against their semantic (User.EMail, User.Password, User.LoginSource, ExternalLoginUser.Login*) and see some problems / limitations for login functionality arise.

Mapping may be more reliable compared to option 1.

classDiagram
  namespace activitypub {
    class ForgeLike {
      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
    }
    class ActorID {
      ID               string
      Source           string
      Schema           string
      Path             string
      Host             string
      Port             string
      UnvalidatedInput string
    }
    class PersonID {
      AsWebfinger() string // "ID@Host"
    }
  }

  ActorID <|-- PersonID
  ForgeLike *-- PersonID: ActorID
  PersonID -- Actor: links to

  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  
    }
  }

  User *-- ExternalLoginUser: ExternalLoginUser.UserID
  User -- Source
  ExternalLoginUser -- Source
  PersonID -- ExternalLoginUser: mapped by AsLoginName() == ExternalID

3. Map to User-&-FederatedUser

  1. We map PersonId.asWbfinger() to FederatedPerson.ExternalID (e.g. 13@some.instan.ce).
  2. Created User is limited:
    1. non functional email is generated, email notification is false.
    2. strong password is generated silently
    3. User.Type is UserTypeRemoteUser
    4. User is not Admin
    5. User is not Active

We can use forgejo code (like star / unstar fkt.) without changes.

Introduce FederatedUser as new model & persistence.

But we use fields (User.EMail, User.Password) against their semantic, but we probably can handle the problems arising.

We will be able to have a reliable mapping.

classDiagram
  namespace activitypub {
    class ForgeLike {
      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
    }
    class ActorID {
      ID               string
      Source           string
      Schema           string
      Path             string
      Host             string
      Port             string
      UnvalidatedInput string
    }
    class PersonID {
      AsLoginName() string // "ID-Host"
      AsWebfinger() string // "@ID@Host"
    }
  }

  ActorID <|-- PersonID
  ForgeLike *-- PersonID: ActorID

  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
      RawData    map[string]any
      ExternalID   string
      FederationHost int64
    }
  }
  User *-- FederatedUser: FederatedUser.UserID
  PersonID -- FederatedUser : mapped by PersonID.asWebfinger() == FederatedUser.externalID

  namespace forgefed {
    
    class FederationHost {
      <<Aggregate Root>>
      ID int64
      HostFqdn string
    }

    class NodeInfo {
      Source string
    }
  }
  FederationHost *-- NodeInfo
  FederatedUser -- FederationHost

 

4. Map to new FederatedPerson and introduce a common User interface

  1. We map PersonId.asWbfinger() to FederatedPerson.ExternalID (e.g. 13@some.instan.ce).
  2. We will have no semantic mismatch.

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.

We will be able to have a reliable mapping.

classDiagram
  namespace activitypub {
    class ForgeLike {
      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
    }
    class ActorID {
      ID               string
      Source           string
      Schema           string
      Path             string
      Host             string
      Port             string
      UnvalidatedInput string
    }
    class PersonID {
      AsLoginName() string // "ID-Host"
      AsWebfinger() string // "@ID@Host"
    }
  }

  ActorID <|-- PersonID
  ForgeLike *-- PersonID: ActorID
  PersonID -- Actor: links to

  namespace user {
    class CommonUser {
      <<Interface>>
    }
    class User {
      
    }    
  }
  User ..<| CommonUser 
  
  namespace forgefed {
    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
    }
  }
  PersonID -- FederatedPerson : mapped by PersonID.asWebfinger() == FederatedPerson.externalID
  FederationHost *-- NodeInfo
  FederatedPerson -- FederationHost
  FederatedPerson ..<| CommonUser