Skip to content

Commit

Permalink
add ory creation webhook (#4629)
Browse files Browse the repository at this point in the history
  • Loading branch information
d-g-town authored May 17, 2024
1 parent bb2b691 commit 432f4c4
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 0 deletions.
132 changes: 132 additions & 0 deletions api/server/handlers/user/create_ory.go
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions api/server/router/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
1 change: 1 addition & 0 deletions api/server/shared/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ type Config struct {

Ory ory.APIClient
OryApiKeyContextWrapper func(ctx context.Context) context.Context
OryActionKey string
}

type ConfigLoader interface {
Expand Down
2 changes: 2 additions & 0 deletions api/server/shared/config/env/envconfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions api/server/shared/config/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down

0 comments on commit 432f4c4

Please sign in to comment.