diff --git a/api/team.go b/api/team.go index 4b12e866d..38977c9b6 100644 --- a/api/team.go +++ b/api/team.go @@ -48,6 +48,7 @@ type Person struct { Email string `json:"email,omitempty"` Avatar string `json:"avatar,omitempty"` Properties PersonProperties `json:"properties,omitempty"` + ExternalID string `json:"external_id,omitempty"` } func (person Person) TableName() string { diff --git a/auth/clerk_client.go b/auth/clerk_client.go index 55441a6af..99148e00a 100644 --- a/auth/clerk_client.go +++ b/auth/clerk_client.go @@ -10,9 +10,9 @@ import ( "github.com/flanksource/commons/logger" "github.com/flanksource/incident-commander/api" "github.com/flanksource/incident-commander/db" - "github.com/golang-jwt/jwt/v4" "github.com/labstack/echo/v4" "github.com/patrickmn/go-cache" + "gorm.io/gorm" ) const ( @@ -50,7 +50,7 @@ func (h ClerkHandler) Session(next echo.HandlerFunc) echo.HandlerFunc { sessionToken := c.Request().Header.Get(echo.HeaderAuthorization) sessionToken = strings.TrimPrefix(sessionToken, "Bearer ") if sessionToken == "" { - // Check for `_session` cookie + // Check for `__session` cookie sessionTokenCookie, err := c.Request().Cookie(clerkSessionCookie) if err != nil { // Cookie not found @@ -59,50 +59,32 @@ func (h ClerkHandler) Session(next echo.HandlerFunc) echo.HandlerFunc { sessionToken = sessionTokenCookie.Value } - user, sessID, err := h.getUser(sessionToken) + ctx := c.(*api.Context) + user, sessID, err := h.getUser(ctx, sessionToken) if err != nil { logger.Errorf("Error fetching user from clerk: %v", err) return c.String(http.StatusUnauthorized, "Unauthorized") } - ctx := c.(*api.Context) - if user.ExternalID == nil { - user, err = h.createDBUser(ctx, user) - if err != nil { - logger.Errorf("Error creating user in database from clerk: %v", err) - return c.String(http.StatusUnauthorized, "Unauthorized") - } - // Clear user from cache so that new metadata is used - h.userCache.Delete(sessID) - } - - token, err := h.getDBToken(sessID, *user.ExternalID) + token, err := h.getDBToken(sessID, user.ID.String()) if err != nil { logger.Errorf("Error generating JWT Token: %v", err) return c.String(http.StatusUnauthorized, "Unauthorized") } c.Request().Header.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) - c.Request().Header.Set(UserIDHeaderKey, *user.ExternalID) + c.Request().Header.Set(UserIDHeaderKey, user.ID.String()) return next(c) } } -func (h *ClerkHandler) generateDBToken(id string) (string, error) { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "role": DefaultPostgrestRole, - "id": id, - }) - return token.SignedString([]byte(h.dbJwtSecret)) -} - func (h *ClerkHandler) getDBToken(sessionID, userID string) (string, error) { cacheKey := sessionID + userID if token, exists := h.tokenCache.Get(cacheKey); exists { return token.(string), nil } // Adding Authorization Token for PostgREST - token, err := h.generateDBToken(userID) + token, err := generateDBToken(h.dbJwtSecret, userID) if err != nil { return "", err } @@ -110,7 +92,7 @@ func (h *ClerkHandler) getDBToken(sessionID, userID string) (string, error) { return token, nil } -func (h *ClerkHandler) getUser(sessionToken string) (*clerk.User, string, error) { +func (h *ClerkHandler) getUser(ctx *api.Context, sessionToken string) (*api.Person, string, error) { sessClaims, err := h.client.VerifyToken(sessionToken) if err != nil { return nil, "", err @@ -118,23 +100,36 @@ func (h *ClerkHandler) getUser(sessionToken string) (*clerk.User, string, error) cacheKey := sessClaims.SessionID if user, exists := h.userCache.Get(cacheKey); exists { - return user.(*clerk.User), sessClaims.SessionID, nil + return user.(*api.Person), sessClaims.SessionID, nil } - user, err := h.client.Users().Read(sessClaims.Claims.Subject) + clerkUser, err := h.client.Users().Read(sessClaims.Claims.Subject) if err != nil { return nil, "", err } - h.userCache.SetDefault(cacheKey, user) - return user, sessClaims.SessionID, nil + + dbUser, err := h.createDBUserIfNotExists(ctx, clerkUser) + if err != nil { + return nil, "", err + } + h.userCache.SetDefault(cacheKey, &dbUser) + return &dbUser, sessClaims.SessionID, nil } -func (h *ClerkHandler) createDBUser(ctx *api.Context, user *clerk.User) (*clerk.User, error) { - if user.ExternalID != nil { - return user, nil +func (h *ClerkHandler) createDBUserIfNotExists(ctx *api.Context, user *clerk.User) (api.Person, error) { + existingUser, err := db.GetUserByExternalID(ctx, user.ID) + if err == nil { + // User with the given clerk ID exists + return existingUser, nil } + + if err != gorm.ErrRecordNotFound { + // Return if any other error, we only want to create the user + return api.Person{}, err + } + if user.PrimaryEmailAddressID == nil { - return nil, fmt.Errorf("clerk.user[%s] has no primary email", user.ID) + return api.Person{}, fmt.Errorf("clerk.user[%s] has no primary email", user.ID) } var email string @@ -152,19 +147,11 @@ func (h *ClerkHandler) createDBUser(ctx *api.Context, user *clerk.User) (*clerk. if user.LastName != nil { name = append(name, *user.LastName) } - person := api.Person{ - Name: strings.Join(name, " "), - Email: email, - } - - dbUser, err := db.GetOrCreateUser(ctx, person) - if err != nil { - return nil, err - } - id := dbUser.ID.String() - userUpdateParams := clerk.UpdateUser{ - ExternalID: &id, + newUser := api.Person{ + Name: strings.Join(name, " "), + Email: email, + ExternalID: user.ID, } - return h.client.Users().Update(user.ID, &userUpdateParams) + return db.CreateUser(ctx, newUser) } diff --git a/auth/middleware.go b/auth/middleware.go index dcc987351..f20ceb701 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -16,7 +16,6 @@ import ( "github.com/flanksource/commons/rand" "github.com/flanksource/commons/utils" "github.com/flanksource/duty/models" - "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" "github.com/labstack/echo/v4" client "github.com/ory/client-go" @@ -260,21 +259,13 @@ func (k *kratosMiddleware) kratosLogin(ctx context.Context, username, password s return &login.Session, nil } -func (k *kratosMiddleware) generateDBToken(id string) (string, error) { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "role": DefaultPostgrestRole, - "id": id, - }) - return token.SignedString([]byte(k.jwtSecret)) -} - func (k *kratosMiddleware) getDBToken(sessionID, userID string) (string, error) { cacheKey := sessionID + userID if token, exists := k.tokenCache.Get(cacheKey); exists { return token.(string), nil } // Adding Authorization Token for PostgREST - token, err := k.generateDBToken(userID) + token, err := generateDBToken(k.jwtSecret, userID) if err != nil { return "", err } diff --git a/auth/utils.go b/auth/utils.go new file mode 100644 index 000000000..48e05fab1 --- /dev/null +++ b/auth/utils.go @@ -0,0 +1,11 @@ +package auth + +import "github.com/golang-jwt/jwt/v4" + +func generateDBToken(secret, id string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "role": DefaultPostgrestRole, + "id": id, + }) + return token.SignedString([]byte(secret)) +} diff --git a/db/people.go b/db/people.go index 80fd6e596..18dd49882 100644 --- a/db/people.go +++ b/db/people.go @@ -38,13 +38,14 @@ func GetUserByID(ctx *api.Context, id string) (api.Person, error) { return user, err } -func GetOrCreateUser(ctx *api.Context, user api.Person) (api.Person, error) { - if err := ctx.DB().Table("people").Where("email = ?", user.Email).Find(&user).Error; err != nil { - return api.Person{}, err - } - if user.ID != uuid.Nil { - return user, nil - } +func GetUserByExternalID(ctx *api.Context, id string) (api.Person, error) { + var user api.Person + err := ctx.DB().Table("people").Where("external_id = ?", id).First(&user).Error + return user, err +} + +// CreateUser creates a new user and returns a copy +func CreateUser(ctx *api.Context, user api.Person) (api.Person, error) { err := ctx.DB().Table("people").Create(&user).Error return user, err } diff --git a/go.mod b/go.mod index 3b4c3210a..f0b5adb2d 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/containrrr/shoutrrr v0.7.1 github.com/fergusstrange/embedded-postgres v1.23.0 github.com/flanksource/commons v1.11.0 - github.com/flanksource/duty v1.0.156 + github.com/flanksource/duty v1.0.157 github.com/flanksource/kopper v1.0.6 github.com/google/cel-go v0.17.2 github.com/google/go-cmp v0.5.9 diff --git a/go.sum b/go.sum index 8c302892b..16c98df0b 100644 --- a/go.sum +++ b/go.sum @@ -765,8 +765,8 @@ github.com/fergusstrange/embedded-postgres v1.23.0 h1:ZYRD89nammxQDWDi6taJE2CYjD github.com/fergusstrange/embedded-postgres v1.23.0/go.mod h1:wL562t1V+iuFwq0UcgMi2e9rp8CROY9wxWZEfP8Y874= github.com/flanksource/commons v1.11.0 h1:ThP3hnX4Xh4thxVl2GjQ92WvQ93jq5VqzJh46jbW23A= github.com/flanksource/commons v1.11.0/go.mod h1:zYEhi6E2+diQ+loVcROUHo/Bgv+Tn61W2NYmrb5MgVI= -github.com/flanksource/duty v1.0.156 h1:d/wcXUm3nP3bOJDnNPEWa0G24HjrxlR+GUHIY78B6s0= -github.com/flanksource/duty v1.0.156/go.mod h1:RJ/kcZ7dbL8/52tem757szVIA3IomS8bOAZIK0xb4rk= +github.com/flanksource/duty v1.0.157 h1:WxOxI+3NGgMXOv6G4t6GXalZgYhIiORBs06FYqg/CLw= +github.com/flanksource/duty v1.0.157/go.mod h1:RJ/kcZ7dbL8/52tem757szVIA3IomS8bOAZIK0xb4rk= github.com/flanksource/gomplate/v3 v3.20.4/go.mod h1:27BNWhzzSjDed1z8YShO6W+z6G9oZXuxfNFGd/iGSdc= github.com/flanksource/gomplate/v3 v3.20.9 h1:I3H/l1FUDepe6IuG8Nj51QNX9ocdU2EGL4GWz31sZdk= github.com/flanksource/gomplate/v3 v3.20.9/go.mod h1:1N1aptaAo0XUaGsyU5CWiwn9GMRpbIKX1AdsypfmZYo=