Bläddra i källkod

add ory creation webhook (#4629)

d-g-town 2 år sedan
förälder
incheckning
432f4c4e14

+ 132 - 0
api/server/handlers/user/create_ory.go

@@ -0,0 +1,132 @@
+package user
+
+import (
+	"errors"
+	"net/http"
+
+	"github.com/porter-dev/porter/internal/analytics"
+
+	"github.com/porter-dev/porter/internal/telemetry"
+
+	"gorm.io/gorm"
+
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/internal/models"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+)
+
+// OryUserCreateHandler is the handler for user creation triggered by an ory action
+type OryUserCreateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewOryUserCreateHandler generates a new OryUserCreateHandler
+func NewOryUserCreateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *OryUserCreateHandler {
+	return &OryUserCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// CreateOryUserRequest is the expected request body for user creation triggered by an ory action
+type CreateOryUserRequest struct {
+	OryId    string `json:"ory_id"`
+	Email    string `json:"email"`
+	Referral string `json:"referral"`
+}
+
+// ServeHTTP handles the user creation triggered by an ory action
+func (u *OryUserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-ory-user")
+	defer span.End()
+
+	// this endpoint is not authenticated through middleware; instead, we check
+	// for the presence of an ory action cookie that matches env
+	oryActionCookie, err := r.Cookie("ory_action")
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "invalid ory action cookie")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
+		return
+	}
+
+	if oryActionCookie.Value != u.Config().OryActionKey {
+		err = telemetry.Error(ctx, span, nil, "cookie does not match")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
+		return
+	}
+
+	request := &CreateOryUserRequest{}
+	ok := u.DecodeAndValidate(w, r, request)
+	if !ok {
+		err = telemetry.Error(ctx, span, nil, "invalid request")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "email", Value: request.Email},
+		telemetry.AttributeKV{Key: "ory-id", Value: request.OryId},
+		telemetry.AttributeKV{Key: "referral", Value: request.Referral},
+	)
+
+	if request.Email == "" {
+		err = telemetry.Error(ctx, span, nil, "email is required")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if request.OryId == "" {
+		err = telemetry.Error(ctx, span, nil, "ory_id is required")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	user := &models.User{
+		Model:         gorm.Model{},
+		Email:         request.Email,
+		EmailVerified: false,
+		AuthProvider:  models.AuthProvider_Ory,
+		ExternalId:    request.OryId,
+	}
+
+	existingUser, err := u.Repo().User().ReadUserByEmail(user.Email)
+	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+		err = telemetry.Error(ctx, span, err, "error reading user by email")
+		u.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if existingUser == nil || existingUser.ID == 0 {
+		user, err = u.Repo().User().CreateUser(user)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error creating user")
+			u.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		_ = u.Config().AnalyticsClient.Identify(analytics.CreateSegmentIdentifyUser(user))
+
+		_ = u.Config().AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
+			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
+			Email:               user.Email,
+			FirstName:           user.FirstName,
+			LastName:            user.LastName,
+			CompanyName:         user.CompanyName,
+			ReferralMethod:      request.Referral,
+		}))
+	} else {
+		existingUser.AuthProvider = models.AuthProvider_Ory
+		existingUser.ExternalId = request.OryId
+		_, err = u.Repo().User().UpdateUser(existingUser)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error updating user")
+			u.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+}

+ 24 - 0
api/server/router/base.go

@@ -197,6 +197,30 @@ func GetBaseRoutes(
 		Router:   r,
 	})
 
+	// POST /api/users/ory -> user.NewOryUserCreateHandler
+	createOryUserEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/users/ory",
+			},
+		},
+	)
+
+	createOryUserHandler := user.NewOryUserCreateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createOryUserEndpoint,
+		Handler:  createOryUserHandler,
+		Router:   r,
+	})
+
 	// POST /api/login -> user.NewUserLoginHandler
 	loginUserEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 1 - 0
api/server/shared/config/config.go

@@ -126,6 +126,7 @@ type Config struct {
 
 	Ory                     ory.APIClient
 	OryApiKeyContextWrapper func(ctx context.Context) context.Context
+	OryActionKey            string
 }
 
 type ConfigLoader interface {

+ 2 - 0
api/server/shared/config/env/envconfs.go

@@ -177,6 +177,8 @@ type ServerConf struct {
 	OryEnabled bool   `env:"ORY_ENABLED,default=false"`
 	OryUrl     string `env:"ORY_URL,default=http://localhost:4000"`
 	OryApiKey  string `env:"ORY_API_KEY"`
+	// OryActionKey is the key used to authenticate api requests from Ory Actions to the Porter API
+	OryActionKey string `env:"ORY_ACTION_KEY"`
 }
 
 // DBConf is the database configuration: if generated from environment variables,

+ 1 - 0
api/server/shared/config/loader/loader.go

@@ -402,6 +402,7 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		res.OryApiKeyContextWrapper = func(ctx context.Context) context.Context {
 			return context.WithValue(ctx, ory.ContextAccessToken, InstanceEnvConf.ServerConf.OryApiKey)
 		}
+		res.OryActionKey = InstanceEnvConf.ServerConf.OryActionKey
 		res.Logger.Info().Msg("Created Ory client")
 	}