migrate.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. package user
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "github.com/porter-dev/porter/api/types"
  9. "github.com/porter-dev/porter/internal/models"
  10. ory "github.com/ory/client-go"
  11. "github.com/porter-dev/porter/internal/telemetry"
  12. "github.com/porter-dev/porter/api/server/handlers"
  13. "github.com/porter-dev/porter/api/server/shared"
  14. "github.com/porter-dev/porter/api/server/shared/apierrors"
  15. "github.com/porter-dev/porter/api/server/shared/config"
  16. )
  17. // MigrateUsersHandler migrates users into Ory
  18. type MigrateUsersHandler struct {
  19. handlers.PorterHandler
  20. }
  21. // NewMigrateUsersHandler generates a handler for migrating users
  22. func NewMigrateUsersHandler(
  23. config *config.Config,
  24. decoderValidator shared.RequestDecoderValidator,
  25. writer shared.ResultWriter,
  26. ) *MigrateUsersHandler {
  27. return &MigrateUsersHandler{
  28. PorterHandler: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  29. }
  30. }
  31. // ServeHTTP migrates users into Ory
  32. func (u *MigrateUsersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  33. ctx, span := telemetry.NewSpan(r.Context(), "serve-migrate-users")
  34. defer span.End()
  35. r = r.Clone(ctx)
  36. thisUser, _ := r.Context().Value(types.UserScope).(*models.User)
  37. if !strings.HasSuffix(thisUser.Email, "@porter.run") {
  38. err := telemetry.Error(ctx, span, nil, "user is not a porter user")
  39. u.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
  40. return
  41. }
  42. users, err := u.Repo().User().ListUsers()
  43. if err != nil {
  44. err := telemetry.Error(ctx, span, nil, "error listing users")
  45. u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  46. return
  47. }
  48. var usersMissingAuthMechanism []uint
  49. migrationErrors := map[string][]uint{}
  50. for _, user := range users {
  51. // skip users that are already migrated
  52. if user.AuthProvider == models.AuthProvider_Ory && user.ExternalId != "" {
  53. continue
  54. }
  55. createIdentityBody := ory.CreateIdentityBody{
  56. SchemaId: "preset://email",
  57. Traits: map[string]interface{}{"email": user.Email},
  58. }
  59. if user.EmailVerified {
  60. createIdentityBody.VerifiableAddresses = []ory.VerifiableIdentityAddress{
  61. {
  62. Value: user.Email,
  63. Verified: true,
  64. Via: "email",
  65. Status: "completed",
  66. },
  67. }
  68. }
  69. switch {
  70. case user.Password != "":
  71. password := user.Password
  72. createIdentityBody.Credentials = &ory.IdentityWithCredentials{
  73. Oidc: nil,
  74. Password: &ory.IdentityWithCredentialsPassword{
  75. Config: &ory.IdentityWithCredentialsPasswordConfig{
  76. HashedPassword: &password,
  77. },
  78. },
  79. AdditionalProperties: nil,
  80. }
  81. case user.GithubUserID != 0:
  82. createIdentityBody.Credentials = &ory.IdentityWithCredentials{
  83. Oidc: &ory.IdentityWithCredentialsOidc{
  84. Config: &ory.IdentityWithCredentialsOidcConfig{
  85. Config: nil,
  86. Providers: []ory.IdentityWithCredentialsOidcConfigProvider{
  87. {
  88. Provider: "github",
  89. Subject: strconv.Itoa(int(user.GithubUserID)),
  90. },
  91. },
  92. },
  93. },
  94. }
  95. case user.GoogleUserID != "":
  96. createIdentityBody.Credentials = &ory.IdentityWithCredentials{
  97. Oidc: &ory.IdentityWithCredentialsOidc{
  98. Config: &ory.IdentityWithCredentialsOidcConfig{
  99. Config: nil,
  100. Providers: []ory.IdentityWithCredentialsOidcConfigProvider{
  101. {
  102. Provider: "google",
  103. Subject: user.GoogleUserID,
  104. },
  105. },
  106. },
  107. },
  108. }
  109. default:
  110. usersMissingAuthMechanism = append(usersMissingAuthMechanism, user.ID)
  111. continue
  112. }
  113. createdIdentity, resp, err := u.Config().Ory.IdentityAPI.CreateIdentity(u.Config().OryApiKeyContextWrapper(ctx)).CreateIdentityBody(createIdentityBody).Execute()
  114. if err != nil {
  115. switch resp.StatusCode {
  116. // identity already exists, so we need to list the identities and find the one that matches
  117. case 409:
  118. identities, _, err := u.Config().Ory.IdentityAPI.ListIdentities(u.Config().OryApiKeyContextWrapper(ctx)).CredentialsIdentifier(user.Email).Execute()
  119. if err != nil {
  120. errString := fmt.Sprintf("error calling list identities``: %v\n", err)
  121. if len(migrationErrors[err.Error()]) == 0 {
  122. migrationErrors[errString] = []uint{}
  123. }
  124. migrationErrors[errString] = append(migrationErrors[errString], user.ID)
  125. continue
  126. }
  127. if len(identities) != 1 {
  128. errString := fmt.Sprintf("expected 1 identity, got %d", len(identities))
  129. if len(migrationErrors[err.Error()]) == 0 {
  130. migrationErrors[errString] = []uint{}
  131. }
  132. migrationErrors[errString] = append(migrationErrors[errString], user.ID)
  133. continue
  134. }
  135. createdIdentity = &identities[0]
  136. default:
  137. errString := fmt.Sprintf("error creating identity: %s", err.Error())
  138. if len(migrationErrors[err.Error()]) == 0 {
  139. migrationErrors[errString] = []uint{}
  140. }
  141. migrationErrors[errString] = append(migrationErrors[errString], user.ID)
  142. continue
  143. }
  144. }
  145. user.AuthProvider = models.AuthProvider_Ory
  146. user.ExternalId = createdIdentity.Id
  147. _, err = u.Repo().User().UpdateUser(user)
  148. if err != nil {
  149. errString := fmt.Sprintf("error updating user: %s", err.Error())
  150. if len(migrationErrors[err.Error()]) == 0 {
  151. migrationErrors[errString] = []uint{}
  152. }
  153. migrationErrors[errString] = append(migrationErrors[errString], user.ID)
  154. continue
  155. }
  156. }
  157. var errs []error
  158. if len(usersMissingAuthMechanism) > 0 {
  159. errs = append(errs, fmt.Errorf("users missing auth mechanism: %v", usersMissingAuthMechanism))
  160. }
  161. for errString, userIds := range migrationErrors {
  162. errs = append(errs, fmt.Errorf("%s: %v", errString, userIds))
  163. }
  164. if len(errs) > 0 {
  165. err := telemetry.Error(ctx, span, errors.Join(errs...), "error migrating users")
  166. u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  167. return
  168. }
  169. }