diff --git a/pkg/coredata/user.go b/pkg/coredata/user.go index 1c6d72d96..b87016bf4 100644 --- a/pkg/coredata/user.go +++ b/pkg/coredata/user.go @@ -16,12 +16,15 @@ package coredata import ( "context" + "errors" "fmt" + "strings" "time" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "go.gearno.de/kit/pg" ) @@ -34,8 +37,24 @@ type ( CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + + ErrUserNotFound struct { + Identifier string + } + + ErrUserAlreadyExists struct { + message string + } ) +func (e ErrUserNotFound) Error() string { + return fmt.Sprintf("user not found: %q", e.Identifier) +} + +func (e ErrUserAlreadyExists) Error() string { + return e.message +} + func (u User) CursorKey() page.CursorKey { return page.NewCursorKey(u.ID, u.CreatedAt) } @@ -69,6 +88,10 @@ LIMIT 1; user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User]) if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return &ErrUserNotFound{Identifier: email} + } + return fmt.Errorf("cannot collect user: %w", err) } @@ -106,6 +129,10 @@ LIMIT 1; user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User]) if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return &ErrUserNotFound{Identifier: userID.String()} + } + return fmt.Errorf("cannot collect user: %w", err) } @@ -141,5 +168,19 @@ VALUES ( } _, err := conn.Exec(ctx, q, args) - return err + + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + if pgErr.Code == "23505" && strings.Contains(pgErr.ConstraintName, "email_address") { + return &ErrUserAlreadyExists{ + message: fmt.Sprintf("user with email %s already exists", u.EmailAddress), + } + } + } + + return err + } + + return nil } diff --git a/pkg/crypto/passwdhash/passwdhash.go b/pkg/crypto/passwdhash/passwdhash.go index 9fe25bc5c..c1955f157 100644 --- a/pkg/crypto/passwdhash/passwdhash.go +++ b/pkg/crypto/passwdhash/passwdhash.go @@ -27,32 +27,34 @@ import ( type ( Profile struct { - minIterations uint - saltLength uint - keyLength uint - pepper []byte + iterations uint32 + saltLength uint + keyLength uint + pepper []byte } ) const ( versionByte = 0x01 // Version identifier algorithmByte = 0x01 // Algorithm identifier (0x01 for PBKDF2-SHA256) + + minIterations = 600000 ) -func NewProfile(pepper []byte) (*Profile, error) { +func NewProfile(pepper []byte, iterations uint32) (*Profile, error) { if len(pepper) < 32 { return nil, fmt.Errorf("pepper must be at least 32 bytes") } - // NIST SP 800-63B recommendations: - // - At least 32 bits of salt (we use 256 bits/32 bytes for extra security) - // - At least 1000 iterations (we use higher based on processing capabilities) - // - Resulting key length should be at least 160 bits (we use 256 bits) + if iterations < minIterations { + return nil, fmt.Errorf("iterations below minimum security threshold") + } + return &Profile{ - minIterations: 600000, // Minimum iterations (adjusted based on hardware speed) - saltLength: 32, // Salt length in bytes (256 bits) - keyLength: 32, // Output key length in bytes (256 bits) - pepper: pepper, // Pepper length in bytes (256 bits) + iterations: iterations, + saltLength: 32, + keyLength: 32, + pepper: pepper, }, nil } @@ -62,14 +64,14 @@ func (hp Profile) applyPepper(input []byte) []byte { return mac.Sum(nil) } -func (hp Profile) HashPassword(password []byte, iterations uint32) ([]byte, error) { +func (hp Profile) HashPassword(password []byte) ([]byte, error) { salt := make([]byte, hp.saltLength) if _, err := rand.Read(salt); err != nil { return nil, fmt.Errorf("error generating salt: %v", err) } pepperedPassword := hp.applyPepper([]byte(password)) - hash := pbkdf2.Key(pepperedPassword, salt, int(iterations), int(hp.keyLength), sha256.New) + hash := pbkdf2.Key(pepperedPassword, salt, int(hp.iterations), int(hp.keyLength), sha256.New) // Binary format: // [1B version][1B algorithm][4B iterations][1B salt length][salt bytes][hash bytes] @@ -81,7 +83,7 @@ func (hp Profile) HashPassword(password []byte, iterations uint32) ([]byte, erro // Iterations (4 bytes, big endian) iterBytes := make([]byte, 4) - binary.BigEndian.PutUint32(iterBytes, iterations) + binary.BigEndian.PutUint32(iterBytes, hp.iterations) binaryHash = append(binaryHash, iterBytes...) // Salt length and salt @@ -110,13 +112,13 @@ func (hp Profile) ComparePasswordAndHash(password, passwordHash []byte) (bool, e // Extract iterations iterations := binary.BigEndian.Uint32(passwordHash[2:6]) - if iterations < uint32(hp.minIterations) { + if iterations < minIterations { return false, fmt.Errorf("iterations below minimum security threshold") } // Extract salt length and validate saltLen := int(passwordHash[6]) - if saltLen < 32 { // NIST minimum requirement + if saltLen < 32 { return false, fmt.Errorf("salt length below security minimum") } diff --git a/pkg/probod/auth_config.go b/pkg/probod/auth_config.go index a1dc7099f..581d0ddb4 100644 --- a/pkg/probod/auth_config.go +++ b/pkg/probod/auth_config.go @@ -21,53 +21,57 @@ import ( type ( authConfig struct { - // Pepper is a secret key used for password hashing - // It should be at least 32 bytes long - Pepper string `json:"pepper"` - SessionDuration int `json:"session-duration"` - CookieName string `json:"cookie-name"` - CookieSecure bool `json:"cookie-secure"` - CookieHTTPOnly bool `json:"cookie-http-only"` - CookieDomain string `json:"cookie-domain"` - CookiePath string `json:"cookie-path"` - CookieSecret string `json:"cookie-secret"` + Cookie cookieConfig `json:"cookie"` + Password passwordConfig `json:"password"` + } + + cookieConfig struct { + Domain string `json:"domain"` + Secret string `json:"secret"` + Duration int `json:"duration"` + Name string `json:"name"` + } + + passwordConfig struct { + Iterations uint32 `json:"iterations"` + Pepper string `json:"pepper"` } ) func (c authConfig) GetPepperBytes() ([]byte, error) { - if c.Pepper == "" { + if c.Password.Pepper == "" { return nil, fmt.Errorf("pepper cannot be empty") } - if decoded, err := base64.StdEncoding.DecodeString(c.Pepper); err == nil { + if decoded, err := base64.StdEncoding.DecodeString(c.Password.Pepper); err == nil { if len(decoded) < 32 { return nil, fmt.Errorf("decoded pepper must be at least 32 bytes long") } return decoded, nil } - if len(c.Pepper) < 32 { + if len(c.Password.Pepper) < 32 { return nil, fmt.Errorf("pepper must be at least 32 bytes long") } - return []byte(c.Pepper), nil + return []byte(c.Password.Pepper), nil } func (c authConfig) GetCookieSecretBytes() ([]byte, error) { - if c.CookieSecret == "" { + if c.Cookie.Secret == "" { return nil, fmt.Errorf("cookie secret cannot be empty") } - if decoded, err := base64.StdEncoding.DecodeString(c.CookieSecret); err == nil { + if decoded, err := base64.StdEncoding.DecodeString(c.Cookie.Secret); err == nil { if len(decoded) < 32 { return nil, fmt.Errorf("decoded cookie secret must be at least 32 bytes long") } return decoded, nil } - if len(c.CookieSecret) < 32 { + if len(c.Cookie.Secret) < 32 { return nil, fmt.Errorf("cookie secret must be at least 32 bytes long") } - return []byte(c.CookieSecret), nil + return []byte(c.Cookie.Secret), nil } diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 37eced0b7..fe11f2c2f 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -26,6 +26,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/getprobo/probo/pkg/awsconfig" "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/crypto/passwdhash" "github.com/getprobo/probo/pkg/probo" "github.com/getprobo/probo/pkg/server" console_v1 "github.com/getprobo/probo/pkg/server/api/console/v1" @@ -75,14 +76,16 @@ func New() *Implm { PoolSize: 100, }, Auth: authConfig{ - Pepper: "this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes", - SessionDuration: 24, - CookieName: "SSID", - CookieSecure: false, - CookieHTTPOnly: true, - CookieDomain: "localhost", - CookiePath: "/", - CookieSecret: "this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes", + Password: passwordConfig{ + Pepper: "this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes", + Iterations: 1000000, + }, + Cookie: cookieConfig{ + Name: "SSID", + Secret: "this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes", + Duration: 24, + Domain: "localhost", + }, }, AWS: awsConfig{ Region: "us-east-1", @@ -153,7 +156,12 @@ func (impl *Implm) Run( return fmt.Errorf("cannot migrate database schema: %w", err) } - usrmgrService, err := usrmgr.NewService(ctx, pgClient, pepper) + hp, err := passwdhash.NewProfile(pepper, uint32(impl.cfg.Auth.Password.Iterations)) + if err != nil { + return fmt.Errorf("cannot create hashing profile: %w", err) + } + + usrmgrService, err := usrmgr.NewService(ctx, pgClient, hp) if err != nil { return fmt.Errorf("cannot create usrmgr service: %w", err) } @@ -169,13 +177,10 @@ func (impl *Implm) Run( Probo: proboService, Usrmgr: usrmgrService, Auth: console_v1.AuthConfig{ - CookieName: impl.cfg.Auth.CookieName, - CookieSecure: impl.cfg.Auth.CookieSecure, - CookieHTTPOnly: impl.cfg.Auth.CookieHTTPOnly, - CookieDomain: impl.cfg.Auth.CookieDomain, - CookiePath: impl.cfg.Auth.CookiePath, - SessionDuration: time.Duration(impl.cfg.Auth.SessionDuration) * time.Hour, - CookieSecret: impl.cfg.Auth.CookieSecret, + CookieName: impl.cfg.Auth.Cookie.Name, + CookieDomain: impl.cfg.Auth.Cookie.Domain, + SessionDuration: time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour, + CookieSecret: impl.cfg.Auth.Cookie.Secret, }, }, ) diff --git a/pkg/securecookie/securecookie.go b/pkg/securecookie/securecookie.go new file mode 100644 index 000000000..c9ae79f46 --- /dev/null +++ b/pkg/securecookie/securecookie.go @@ -0,0 +1,164 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package securecookie + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "net/http" + "strings" + "time" +) + +var ( + ErrInvalidCookie = errors.New("invalid cookie") + ErrCookieNotFound = errors.New("cookie not found") + ErrInvalidSignature = errors.New("invalid signature") +) + +// Config holds the configuration for secure cookies +type Config struct { + // Name is the name of the cookie + Name string + + // Secret is the secret key used for signing cookies + Secret string + + // Domain is the cookie domain + Domain string + + // Path is the cookie path + Path string + + // MaxAge is the maximum age of the cookie in seconds + MaxAge int + + // Secure indicates if the cookie should only be sent over HTTPS + Secure bool + + // HTTPOnly indicates if the cookie should be inaccessible to JavaScript + HTTPOnly bool + + // SameSite defines the SameSite attribute of the cookie + SameSite http.SameSite +} + +// DefaultConfig returns a default secure cookie configuration +func DefaultConfig(name, secret string) Config { + return Config{ + Name: name, + Secret: secret, + Path: "/", + MaxAge: 86400 * 30, // 30 days + Secure: true, + HTTPOnly: true, + SameSite: http.SameSiteStrictMode, + } +} + +// Set creates and sets a secure cookie with the given value +func Set(w http.ResponseWriter, config Config, value string) error { + signedValue, err := Sign(value, config.Secret) + if err != nil { + return fmt.Errorf("failed to sign cookie value: %w", err) + } + + cookie := &http.Cookie{ + Name: config.Name, + Value: signedValue, + Path: config.Path, + Domain: config.Domain, + MaxAge: config.MaxAge, + Secure: config.Secure, + HttpOnly: config.HTTPOnly, + SameSite: config.SameSite, + } + + http.SetCookie(w, cookie) + return nil +} + +// Get retrieves and verifies a secure cookie +func Get(r *http.Request, config Config) (string, error) { + cookie, err := r.Cookie(config.Name) + if err != nil { + return "", ErrCookieNotFound + } + + value, err := Verify(cookie.Value, config.Secret) + if err != nil { + return "", ErrInvalidCookie + } + + return value, nil +} + +// Clear removes a cookie by setting its expiration in the past +func Clear(w http.ResponseWriter, config Config) { + cookie := &http.Cookie{ + Name: config.Name, + Value: "", + Path: config.Path, + Domain: config.Domain, + MaxAge: -1, + Expires: time.Now().Add(-1 * time.Hour), + Secure: config.Secure, + HttpOnly: config.HTTPOnly, + SameSite: config.SameSite, + } + http.SetCookie(w, cookie) +} + +// Sign creates a signed value using HMAC-SHA256 +func Sign(value, secret string) (string, error) { + if secret == "" { + return "", fmt.Errorf("secret cannot be empty") + } + + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(value)) + + signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + + return value + "." + signature, nil +} + +// Verify checks if a signed value is valid +func Verify(signedValue, secret string) (string, error) { + if secret == "" { + return "", fmt.Errorf("secret cannot be empty") + } + + parts := strings.Split(signedValue, ".") + if len(parts) != 2 { + return "", fmt.Errorf("invalid signed value format") + } + + value := parts[0] + + expectedSignedValue, err := Sign(value, secret) + if err != nil { + return "", fmt.Errorf("failed to sign value: %w", err) + } + + if signedValue != expectedSignedValue { + return "", ErrInvalidSignature + } + + return value, nil +} diff --git a/pkg/server/api/console/v1/auth_handlers.go b/pkg/server/api/console/v1/auth_handlers.go index a6ef2c4e7..13a1db572 100644 --- a/pkg/server/api/console/v1/auth_handlers.go +++ b/pkg/server/api/console/v1/auth_handlers.go @@ -1,215 +1,21 @@ package console_v1 import ( - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/usrmgr" "github.com/go-chi/chi/v5" ) type ( - // RegisterRequest represents the request body for user registration RegisterRequest struct { Email string `json:"email"` Password string `json:"password"` FullName string `json:"fullName"` } - - // LoginRequest represents the request body for user login - LoginRequest struct { - Email string `json:"email"` - Password string `json:"password"` - } - - // AuthResponse represents the response for successful authentication - AuthResponse struct { - User UserResponse `json:"user"` - Session SessionResponse `json:"session"` - } - - // UserResponse represents user data in the authentication response - UserResponse struct { - ID gid.GID `json:"id"` - Email string `json:"email"` - FullName string `json:"fullName"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - } - - // SessionResponse represents session data in the authentication response - SessionResponse struct { - ID gid.GID `json:"id"` - ExpiresAt time.Time `json:"expiresAt"` - } ) -// RegisterHandler handles user registration -func RegisterHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // Parse request body - var req RegisterRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - // Validate request - if req.Email == "" || req.Password == "" { - http.Error(w, "Email and password are required", http.StatusBadRequest) - return - } - - // Register the user - user, err := usrmgrSvc.RegisterUser( - r.Context(), - usrmgr.RegisterUserParams{ - Email: req.Email, - Password: req.Password, - FullName: req.FullName, - }, - ) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to register user: %v", err), http.StatusInternalServerError) - return - } - - // Log the user in - session, err := usrmgrSvc.Login(r.Context(), req.Email, req.Password) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to login after registration: %v", err), http.StatusInternalServerError) - return - } - - // Set the session cookie - setSessionCookie(w, session.ID.String(), authCfg) - - // Return response - resp := AuthResponse{ - User: UserResponse{ - ID: user.ID, - Email: user.EmailAddress, - FullName: user.FullName, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - }, - Session: SessionResponse{ - ID: session.ID, - ExpiresAt: session.ExpiredAt, - }, - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return - } - } -} - -// LoginHandler handles user login -func LoginHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // Parse request body - var req LoginRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - // Validate request - if req.Email == "" || req.Password == "" { - http.Error(w, "Email and password are required", http.StatusBadRequest) - return - } - - // Login the user - session, err := usrmgrSvc.Login(r.Context(), req.Email, req.Password) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to login: %v", err), http.StatusUnauthorized) - return - } - - // Get the user - user, err := usrmgrSvc.GetUserBySession(r.Context(), session.ID) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to get user: %v", err), http.StatusInternalServerError) - return - } - - // Set the session cookie - setSessionCookie(w, session.ID.String(), authCfg) - - // Return response - resp := AuthResponse{ - User: UserResponse{ - ID: user.ID, - Email: user.EmailAddress, - FullName: user.FullName, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - }, - Session: SessionResponse{ - ID: session.ID, - ExpiresAt: session.ExpiredAt, - }, - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return - } - } -} - -// LogoutHandler handles user logout -func LogoutHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // Get session from cookie - cookie, err := r.Cookie(authCfg.CookieName) - if err != nil || cookie.Value == "" { - http.Error(w, "No active session", http.StatusBadRequest) - return - } - - // Verify the cookie signature - originalValue, err := verifyCookieValue(cookie.Value, authCfg.CookieSecret) - if err != nil { - http.Error(w, "Invalid session cookie", http.StatusBadRequest) - return - } - - // Parse the session ID - sessionID, err := gid.ParseGID(originalValue) - if err != nil { - http.Error(w, "Invalid session ID", http.StatusBadRequest) - return - } - - // Logout the user - err = usrmgrSvc.Logout(r.Context(), sessionID) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to logout: %v", err), http.StatusInternalServerError) - return - } - - // Clear the session cookie - clearSessionCookie(w, authCfg) - - // Return success response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]bool{"success": true}) - } -} - // RegisterAuthRoutes registers the authentication routes func RegisterAuthRoutes(r chi.Router, usrmgrSvc *usrmgr.Service, authCfg AuthConfig) { - r.Post("/auth/register", RegisterHandler(usrmgrSvc, authCfg)) - r.Post("/auth/login", LoginHandler(usrmgrSvc, authCfg)) - r.Post("/auth/logout", LogoutHandler(usrmgrSvc, authCfg)) + r.Post("/auth/register", SignUpHandler(usrmgrSvc, authCfg)) + r.Post("/auth/login", SignInHandler(usrmgrSvc, authCfg)) + r.Post("/auth/logout", SignOutHandler(usrmgrSvc, authCfg)) } diff --git a/pkg/server/api/console/v1/cookie.go b/pkg/server/api/console/v1/cookie.go deleted file mode 100644 index 775c1a16a..000000000 --- a/pkg/server/api/console/v1/cookie.go +++ /dev/null @@ -1,38 +0,0 @@ -package console_v1 - -import ( - "net/http" -) - -// setSessionCookie sets a session cookie in the response -func setSessionCookie(w http.ResponseWriter, sessionID string, cfg AuthConfig) { - // Sign the session ID - signedValue := signCookieValue(sessionID, cfg.CookieSecret) - - cookie := &http.Cookie{ - Name: cfg.CookieName, - Value: signedValue, - Path: cfg.CookiePath, - Domain: cfg.CookieDomain, - Secure: cfg.CookieSecure, - HttpOnly: cfg.CookieHTTPOnly, - MaxAge: int(cfg.SessionDuration.Seconds()), - SameSite: http.SameSiteLaxMode, - } - http.SetCookie(w, cookie) -} - -// clearSessionCookie clears the session cookie -func clearSessionCookie(w http.ResponseWriter, cfg AuthConfig) { - cookie := &http.Cookie{ - Name: cfg.CookieName, - Value: "", - Path: cfg.CookiePath, - Domain: cfg.CookieDomain, - Secure: cfg.CookieSecure, - HttpOnly: cfg.CookieHTTPOnly, - MaxAge: -1, - SameSite: http.SameSiteLaxMode, - } - http.SetCookie(w, cookie) -} diff --git a/pkg/server/api/console/v1/cookie_signer.go b/pkg/server/api/console/v1/cookie_signer.go deleted file mode 100644 index 7be8dae22..000000000 --- a/pkg/server/api/console/v1/cookie_signer.go +++ /dev/null @@ -1,50 +0,0 @@ -package console_v1 - -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "fmt" - "strings" -) - -func signCookieValue(value string, secret string) string { - if secret == "" { - panic(fmt.Errorf("cookie secret is not set")) - } - - h := hmac.New(sha256.New, []byte(secret)) - - h.Write([]byte(value)) - - signature := base64.URLEncoding.EncodeToString(h.Sum(nil)) - - return fmt.Sprintf("%s.%s", value, signature) -} - -func verifyCookieValue(signedValue string, secret string) (string, error) { - if secret == "" { - panic(fmt.Errorf("cookie secret is not set")) - } - - parts := strings.Split(signedValue, ".") - if len(parts) != 2 { - return "", fmt.Errorf("invalid signed cookie format") - } - - value, signature := parts[0], parts[1] - - expectedSignedValue := signCookieValue(value, secret) - expectedParts := strings.Split(expectedSignedValue, ".") - if len(expectedParts) != 2 { - return "", fmt.Errorf("error computing signature") - } - - expectedSignature := expectedParts[1] - - if signature != expectedSignature { - return "", fmt.Errorf("cookie signature verification failed") - } - - return value, nil -} diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index cce28e2c0..2a99098ce 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -18,6 +18,7 @@ package console_v1 import ( "context" + "errors" "fmt" "net/http" "time" @@ -30,6 +31,7 @@ import ( "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/probo" + "github.com/getprobo/probo/pkg/securecookie" "github.com/getprobo/probo/pkg/server/api/console/v1/schema" "github.com/getprobo/probo/pkg/usrmgr" "github.com/go-chi/chi/v5" @@ -39,10 +41,7 @@ import ( type ( AuthConfig struct { CookieName string - CookieSecure bool - CookieHTTPOnly bool CookieDomain string - CookiePath string SessionDuration time.Duration CookieSecret string } @@ -141,28 +140,29 @@ func graphqlHandler(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg } ctx := context.WithValue(r.Context(), httpContextKey, httpCtx) - // Extract session from cookie - cookie, err := r.Cookie(authCfg.CookieName) - if err == nil && cookie.Value != "" { - // Verify the cookie signature - originalValue, err := verifyCookieValue(cookie.Value, authCfg.CookieSecret) - if err == nil { - // Parse the session ID - sessionID, err := gid.ParseGID(originalValue) - if err == nil { - // Get the session - session, err := usrmgrSvc.GetSession(r.Context(), sessionID) - if err == nil { - // Add session to context - ctx = context.WithValue(ctx, sessionContextKey, session) + cookieValue, err := securecookie.Get(r, securecookie.DefaultConfig( + authCfg.CookieName, + authCfg.CookieSecret, + )) + if err != nil { + if !errors.Is(err, securecookie.ErrCookieNotFound) { + panic(fmt.Errorf("failed to get session: %w", err)) + } + } - // Get the user - user, err := usrmgrSvc.GetUserBySession(r.Context(), sessionID) - if err == nil { - // Add user to context - ctx = context.WithValue(ctx, userContextKey, user) - } - } + sessionID, err := gid.ParseGID(cookieValue) + if err == nil { + // Get the session + session, err := usrmgrSvc.GetSession(r.Context(), sessionID) + if err == nil { + // Add session to context + ctx = context.WithValue(ctx, sessionContextKey, session) + + // Get the user + user, err := usrmgrSvc.GetUserBySession(r.Context(), sessionID) + if err == nil { + // Add user to context + ctx = context.WithValue(ctx, userContextKey, user) } } } diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 182f4858a..d58c7f1ff 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -14,9 +14,9 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" + "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" - "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/server/api/console/v1/types" gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" @@ -1845,20 +1845,14 @@ enum ControlState enum TaskState @goModel(model: "github.com/getprobo/probo/pkg/coredata.TaskState") { - TODO - @goEnum(value: "github.com/getprobo/probo/pkg/coredata.TaskStateTodo") - DONE - @goEnum(value: "github.com/getprobo/probo/pkg/coredata.TaskStateDone") + TODO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.TaskStateTodo") + DONE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.TaskStateDone") } enum EvidenceState - @goModel( - model: "github.com/getprobo/probo/pkg/coredata.EvidenceState" - ) { + @goModel(model: "github.com/getprobo/probo/pkg/coredata.EvidenceState") { VALID - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateValid" - ) + @goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateValid") INVALID @goEnum( value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateInvalid" @@ -1872,9 +1866,7 @@ enum EvidenceState enum PeopleKind @goModel(model: "github.com/getprobo/probo/pkg/coredata.PeopleKind") { EMPLOYEE - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.PeopleKindEmployee" - ) + @goEnum(value: "github.com/getprobo/probo/pkg/coredata.PeopleKindEmployee") CONTRACTOR @goEnum( value: "github.com/getprobo/probo/pkg/coredata.PeopleKindContractor" @@ -2181,9 +2173,7 @@ input UpdatePeopleInput { } enum ServiceCriticality - @goModel( - model: "github.com/getprobo/probo/pkg/coredata.ServiceCriticality" - ) { + @goModel(model: "github.com/getprobo/probo/pkg/coredata.ServiceCriticality") { LOW @goEnum( value: "github.com/getprobo/probo/pkg/coredata.ServiceCriticalityLow" @@ -2201,17 +2191,11 @@ enum ServiceCriticality enum RiskTier @goModel(model: "github.com/getprobo/probo/pkg/coredata.RiskTier") { CRITICAL - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.RiskTierCritical" - ) + @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RiskTierCritical") SIGNIFICANT - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.RiskTierSignificant" - ) + @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RiskTierSignificant") GENERAL - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.RiskTierGeneral" - ) + @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RiskTierGeneral") } input UpdateVendorInput { @@ -2352,13 +2336,9 @@ type DeleteEvidencePayload { enum PolicyStatus @goModel(model: "github.com/getprobo/probo/pkg/coredata.PolicyStatus") { DRAFT - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.PolicyStatusDraft" - ) + @goEnum(value: "github.com/getprobo/probo/pkg/coredata.PolicyStatusDraft") ACTIVE - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.PolicyStatusActive" - ) + @goEnum(value: "github.com/getprobo/probo/pkg/coredata.PolicyStatusActive") } input CreatePolicyInput { @@ -3871,7 +3851,7 @@ func (ec *executionContext) _Control_state(ctx context.Context, field graphql.Co } res := resTmp.(coredata.ControlState) fc.Result = res - return ec.marshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState(ctx, field.Selections, res) + return ec.marshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Control_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -4910,7 +4890,7 @@ func (ec *executionContext) _Evidence_state(ctx context.Context, field graphql.C } res := resTmp.(coredata.EvidenceState) fc.Result = res - return ec.marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐEvidenceState(ctx, field.Selections, res) + return ec.marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceState(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Evidence_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -7521,7 +7501,7 @@ func (ec *executionContext) _People_kind(ctx context.Context, field graphql.Coll } res := resTmp.(coredata.PeopleKind) fc.Result = res - return ec.marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind(ctx, field.Selections, res) + return ec.marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_People_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -7973,7 +7953,7 @@ func (ec *executionContext) _Policy_status(ctx context.Context, field graphql.Co } res := resTmp.(coredata.PolicyStatus) fc.Result = res - return ec.marshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus(ctx, field.Selections, res) + return ec.marshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Policy_status(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -8840,7 +8820,7 @@ func (ec *executionContext) _Task_state(ctx context.Context, field graphql.Colle } res := resTmp.(coredata.TaskState) fc.Result = res - return ec.marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState(ctx, field.Selections, res) + return ec.marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Task_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -10007,7 +9987,7 @@ func (ec *executionContext) _Vendor_serviceCriticality(ctx context.Context, fiel } res := resTmp.(coredata.ServiceCriticality) fc.Result = res - return ec.marshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality(ctx, field.Selections, res) + return ec.marshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Vendor_serviceCriticality(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -10045,7 +10025,7 @@ func (ec *executionContext) _Vendor_riskTier(ctx context.Context, field graphql. } res := resTmp.(coredata.RiskTier) fc.Result = res - return ec.marshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier(ctx, field.Selections, res) + return ec.marshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Vendor_riskTier(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -12335,7 +12315,7 @@ func (ec *executionContext) unmarshalInputCreatePeopleInput(ctx context.Context, it.AdditionalEmailAddresses = data case "kind": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("kind")) - data, err := ec.unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind(ctx, v) + data, err := ec.unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind(ctx, v) if err != nil { return it, err } @@ -12383,7 +12363,7 @@ func (ec *executionContext) unmarshalInputCreatePolicyInput(ctx context.Context, it.Content = data case "status": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("status")) - data, err := ec.unmarshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus(ctx, v) + data, err := ec.unmarshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus(ctx, v) if err != nil { return it, err } @@ -12500,14 +12480,14 @@ func (ec *executionContext) unmarshalInputCreateVendorInput(ctx context.Context, it.ServiceTerminationAt = data case "serviceCriticality": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("serviceCriticality")) - data, err := ec.unmarshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality(ctx, v) + data, err := ec.unmarshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality(ctx, v) if err != nil { return it, err } it.ServiceCriticality = data case "riskTier": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("riskTier")) - data, err := ec.unmarshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier(ctx, v) + data, err := ec.unmarshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier(ctx, v) if err != nil { return it, err } @@ -12752,7 +12732,7 @@ func (ec *executionContext) unmarshalInputUpdateControlInput(ctx context.Context it.Category = data case "state": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state")) - data, err := ec.unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState(ctx, v) + data, err := ec.unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState(ctx, v) if err != nil { return it, err } @@ -12862,7 +12842,7 @@ func (ec *executionContext) unmarshalInputUpdatePeopleInput(ctx context.Context, it.AdditionalEmailAddresses = data case "kind": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("kind")) - data, err := ec.unmarshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind(ctx, v) + data, err := ec.unmarshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind(ctx, v) if err != nil { return it, err } @@ -12917,7 +12897,7 @@ func (ec *executionContext) unmarshalInputUpdatePolicyInput(ctx context.Context, it.Content = data case "status": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("status")) - data, err := ec.unmarshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus(ctx, v) + data, err := ec.unmarshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus(ctx, v) if err != nil { return it, err } @@ -12986,7 +12966,7 @@ func (ec *executionContext) unmarshalInputUpdateTaskInput(ctx context.Context, o it.Description = data case "state": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state")) - data, err := ec.unmarshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState(ctx, v) + data, err := ec.unmarshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState(ctx, v) if err != nil { return it, err } @@ -13055,14 +13035,14 @@ func (ec *executionContext) unmarshalInputUpdateVendorInput(ctx context.Context, it.ServiceTerminationAt = data case "serviceCriticality": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("serviceCriticality")) - data, err := ec.unmarshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality(ctx, v) + data, err := ec.unmarshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality(ctx, v) if err != nil { return it, err } it.ServiceCriticality = data case "riskTier": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("riskTier")) - data, err := ec.unmarshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier(ctx, v) + data, err := ec.unmarshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier(ctx, v) if err != nil { return it, err } @@ -16415,14 +16395,14 @@ func (ec *executionContext) marshalNControlEdge2ᚖgithubᚗcomᚋgetproboᚋpro return ec._ControlEdge(ctx, sel, v) } -func (ec *executionContext) unmarshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState(ctx context.Context, v any) (coredata.ControlState, error) { +func (ec *executionContext) unmarshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState(ctx context.Context, v any) (coredata.ControlState, error) { tmp, err := graphql.UnmarshalString(v) - res := unmarshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState[tmp] + res := unmarshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState[tmp] return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState(ctx context.Context, sel ast.SelectionSet, v coredata.ControlState) graphql.Marshaler { - res := graphql.MarshalString(marshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState[v]) +func (ec *executionContext) marshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState(ctx context.Context, sel ast.SelectionSet, v coredata.ControlState) graphql.Marshaler { + res := graphql.MarshalString(marshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState[v]) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -16432,13 +16412,13 @@ func (ec *executionContext) marshalNControlState2githubᚗcomᚋgetproboᚋprobo } var ( - unmarshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState = map[string]coredata.ControlState{ + unmarshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState = map[string]coredata.ControlState{ "NOT_STARTED": coredata.ControlStateNotStarted, "IN_PROGRESS": coredata.ControlStateInProgress, "NOT_APPLICABLE": coredata.ControlStateNotApplicable, "IMPLEMENTED": coredata.ControlStateImplemented, } - marshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState = map[coredata.ControlState]string{ + marshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState = map[coredata.ControlState]string{ coredata.ControlStateNotStarted: "NOT_STARTED", coredata.ControlStateInProgress: "IN_PROGRESS", coredata.ControlStateNotApplicable: "NOT_APPLICABLE", @@ -16795,14 +16775,14 @@ func (ec *executionContext) marshalNEvidenceEdge2ᚖgithubᚗcomᚋgetproboᚋpr return ec._EvidenceEdge(ctx, sel, v) } -func (ec *executionContext) unmarshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐEvidenceState(ctx context.Context, v any) (coredata.EvidenceState, error) { +func (ec *executionContext) unmarshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceState(ctx context.Context, v any) (coredata.EvidenceState, error) { tmp, err := graphql.UnmarshalString(v) - res := unmarshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐEvidenceState[tmp] + res := unmarshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceState[tmp] return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐEvidenceState(ctx context.Context, sel ast.SelectionSet, v coredata.EvidenceState) graphql.Marshaler { - res := graphql.MarshalString(marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐEvidenceState[v]) +func (ec *executionContext) marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceState(ctx context.Context, sel ast.SelectionSet, v coredata.EvidenceState) graphql.Marshaler { + res := graphql.MarshalString(marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceState[v]) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -16812,12 +16792,12 @@ func (ec *executionContext) marshalNEvidenceState2githubᚗcomᚋgetproboᚋprob } var ( - unmarshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐEvidenceState = map[string]coredata.EvidenceState{ + unmarshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceState = map[string]coredata.EvidenceState{ "VALID": coredata.EvidenceStateValid, "INVALID": coredata.EvidenceStateInvalid, "EXPIRED": coredata.EvidenceStateExpired, } - marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐEvidenceState = map[coredata.EvidenceState]string{ + marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceState = map[coredata.EvidenceState]string{ coredata.EvidenceStateValid: "VALID", coredata.EvidenceStateInvalid: "INVALID", coredata.EvidenceStateExpired: "EXPIRED", @@ -17094,14 +17074,14 @@ func (ec *executionContext) marshalNPeopleEdge2ᚖgithubᚗcomᚋgetproboᚋprob return ec._PeopleEdge(ctx, sel, v) } -func (ec *executionContext) unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind(ctx context.Context, v any) (coredata.PeopleKind, error) { +func (ec *executionContext) unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind(ctx context.Context, v any) (coredata.PeopleKind, error) { tmp, err := graphql.UnmarshalString(v) - res := unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind[tmp] + res := unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind[tmp] return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind(ctx context.Context, sel ast.SelectionSet, v coredata.PeopleKind) graphql.Marshaler { - res := graphql.MarshalString(marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind[v]) +func (ec *executionContext) marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind(ctx context.Context, sel ast.SelectionSet, v coredata.PeopleKind) graphql.Marshaler { + res := graphql.MarshalString(marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind[v]) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -17111,11 +17091,11 @@ func (ec *executionContext) marshalNPeopleKind2githubᚗcomᚋgetproboᚋprobo } var ( - unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind = map[string]coredata.PeopleKind{ + unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind = map[string]coredata.PeopleKind{ "EMPLOYEE": coredata.PeopleKindEmployee, "CONTRACTOR": coredata.PeopleKindContractor, } - marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind = map[coredata.PeopleKind]string{ + marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind = map[coredata.PeopleKind]string{ coredata.PeopleKindEmployee: "EMPLOYEE", coredata.PeopleKindContractor: "CONTRACTOR", } @@ -17193,14 +17173,14 @@ func (ec *executionContext) marshalNPolicyEdge2ᚖgithubᚗcomᚋgetproboᚋprob return ec._PolicyEdge(ctx, sel, v) } -func (ec *executionContext) unmarshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus(ctx context.Context, v any) (coredata.PolicyStatus, error) { +func (ec *executionContext) unmarshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus(ctx context.Context, v any) (coredata.PolicyStatus, error) { tmp, err := graphql.UnmarshalString(v) - res := unmarshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus[tmp] + res := unmarshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus[tmp] return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus(ctx context.Context, sel ast.SelectionSet, v coredata.PolicyStatus) graphql.Marshaler { - res := graphql.MarshalString(marshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus[v]) +func (ec *executionContext) marshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus(ctx context.Context, sel ast.SelectionSet, v coredata.PolicyStatus) graphql.Marshaler { + res := graphql.MarshalString(marshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus[v]) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -17210,24 +17190,24 @@ func (ec *executionContext) marshalNPolicyStatus2githubᚗcomᚋgetproboᚋprobo } var ( - unmarshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus = map[string]coredata.PolicyStatus{ + unmarshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus = map[string]coredata.PolicyStatus{ "DRAFT": coredata.PolicyStatusDraft, "ACTIVE": coredata.PolicyStatusActive, } - marshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus = map[coredata.PolicyStatus]string{ + marshalNPolicyStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus = map[coredata.PolicyStatus]string{ coredata.PolicyStatusDraft: "DRAFT", coredata.PolicyStatusActive: "ACTIVE", } ) -func (ec *executionContext) unmarshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier(ctx context.Context, v any) (coredata.RiskTier, error) { +func (ec *executionContext) unmarshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier(ctx context.Context, v any) (coredata.RiskTier, error) { tmp, err := graphql.UnmarshalString(v) - res := unmarshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier[tmp] + res := unmarshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier[tmp] return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier(ctx context.Context, sel ast.SelectionSet, v coredata.RiskTier) graphql.Marshaler { - res := graphql.MarshalString(marshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier[v]) +func (ec *executionContext) marshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier(ctx context.Context, sel ast.SelectionSet, v coredata.RiskTier) graphql.Marshaler { + res := graphql.MarshalString(marshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier[v]) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -17237,26 +17217,26 @@ func (ec *executionContext) marshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋp } var ( - unmarshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier = map[string]coredata.RiskTier{ + unmarshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier = map[string]coredata.RiskTier{ "CRITICAL": coredata.RiskTierCritical, "SIGNIFICANT": coredata.RiskTierSignificant, "GENERAL": coredata.RiskTierGeneral, } - marshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier = map[coredata.RiskTier]string{ + marshalNRiskTier2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier = map[coredata.RiskTier]string{ coredata.RiskTierCritical: "CRITICAL", coredata.RiskTierSignificant: "SIGNIFICANT", coredata.RiskTierGeneral: "GENERAL", } ) -func (ec *executionContext) unmarshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality(ctx context.Context, v any) (coredata.ServiceCriticality, error) { +func (ec *executionContext) unmarshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality(ctx context.Context, v any) (coredata.ServiceCriticality, error) { tmp, err := graphql.UnmarshalString(v) - res := unmarshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality[tmp] + res := unmarshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality[tmp] return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality(ctx context.Context, sel ast.SelectionSet, v coredata.ServiceCriticality) graphql.Marshaler { - res := graphql.MarshalString(marshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality[v]) +func (ec *executionContext) marshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality(ctx context.Context, sel ast.SelectionSet, v coredata.ServiceCriticality) graphql.Marshaler { + res := graphql.MarshalString(marshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality[v]) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -17266,12 +17246,12 @@ func (ec *executionContext) marshalNServiceCriticality2githubᚗcomᚋgetprobo } var ( - unmarshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality = map[string]coredata.ServiceCriticality{ + unmarshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality = map[string]coredata.ServiceCriticality{ "LOW": coredata.ServiceCriticalityLow, "MEDIUM": coredata.ServiceCriticalityMedium, "HIGH": coredata.ServiceCriticalityHigh, } - marshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality = map[coredata.ServiceCriticality]string{ + marshalNServiceCriticality2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality = map[coredata.ServiceCriticality]string{ coredata.ServiceCriticalityLow: "LOW", coredata.ServiceCriticalityMedium: "MEDIUM", coredata.ServiceCriticalityHigh: "HIGH", @@ -17397,14 +17377,14 @@ func (ec *executionContext) marshalNTaskEdge2ᚖgithubᚗcomᚋgetproboᚋprobo return ec._TaskEdge(ctx, sel, v) } -func (ec *executionContext) unmarshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState(ctx context.Context, v any) (coredata.TaskState, error) { +func (ec *executionContext) unmarshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState(ctx context.Context, v any) (coredata.TaskState, error) { tmp, err := graphql.UnmarshalString(v) - res := unmarshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState[tmp] + res := unmarshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState[tmp] return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState(ctx context.Context, sel ast.SelectionSet, v coredata.TaskState) graphql.Marshaler { - res := graphql.MarshalString(marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState[v]) +func (ec *executionContext) marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState(ctx context.Context, sel ast.SelectionSet, v coredata.TaskState) graphql.Marshaler { + res := graphql.MarshalString(marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState[v]) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -17414,11 +17394,11 @@ func (ec *executionContext) marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋ } var ( - unmarshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState = map[string]coredata.TaskState{ + unmarshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState = map[string]coredata.TaskState{ "TODO": coredata.TaskStateTodo, "DONE": coredata.TaskStateDone, } - marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState = map[coredata.TaskState]string{ + marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState = map[coredata.TaskState]string{ coredata.TaskStateTodo: "TODO", coredata.TaskStateDone: "DONE", } @@ -17913,31 +17893,31 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } -func (ec *executionContext) unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState(ctx context.Context, v any) (*coredata.ControlState, error) { +func (ec *executionContext) unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState(ctx context.Context, v any) (*coredata.ControlState, error) { if v == nil { return nil, nil } tmp, err := graphql.UnmarshalString(v) - res := unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState[tmp] + res := unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState[tmp] return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState(ctx context.Context, sel ast.SelectionSet, v *coredata.ControlState) graphql.Marshaler { +func (ec *executionContext) marshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState(ctx context.Context, sel ast.SelectionSet, v *coredata.ControlState) graphql.Marshaler { if v == nil { return graphql.Null } - res := graphql.MarshalString(marshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState[*v]) + res := graphql.MarshalString(marshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState[*v]) return res } var ( - unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState = map[string]coredata.ControlState{ + unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState = map[string]coredata.ControlState{ "NOT_STARTED": coredata.ControlStateNotStarted, "IN_PROGRESS": coredata.ControlStateInProgress, "NOT_APPLICABLE": coredata.ControlStateNotApplicable, "IMPLEMENTED": coredata.ControlStateImplemented, } - marshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState = map[coredata.ControlState]string{ + marshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐControlState = map[coredata.ControlState]string{ coredata.ControlStateNotStarted: "NOT_STARTED", coredata.ControlStateInProgress: "IN_PROGRESS", coredata.ControlStateNotApplicable: "NOT_APPLICABLE", @@ -18009,116 +17989,116 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele return res } -func (ec *executionContext) unmarshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind(ctx context.Context, v any) (*coredata.PeopleKind, error) { +func (ec *executionContext) unmarshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind(ctx context.Context, v any) (*coredata.PeopleKind, error) { if v == nil { return nil, nil } tmp, err := graphql.UnmarshalString(v) - res := unmarshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind[tmp] + res := unmarshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind[tmp] return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind(ctx context.Context, sel ast.SelectionSet, v *coredata.PeopleKind) graphql.Marshaler { +func (ec *executionContext) marshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind(ctx context.Context, sel ast.SelectionSet, v *coredata.PeopleKind) graphql.Marshaler { if v == nil { return graphql.Null } - res := graphql.MarshalString(marshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind[*v]) + res := graphql.MarshalString(marshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind[*v]) return res } var ( - unmarshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind = map[string]coredata.PeopleKind{ + unmarshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind = map[string]coredata.PeopleKind{ "EMPLOYEE": coredata.PeopleKindEmployee, "CONTRACTOR": coredata.PeopleKindContractor, } - marshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind = map[coredata.PeopleKind]string{ + marshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind = map[coredata.PeopleKind]string{ coredata.PeopleKindEmployee: "EMPLOYEE", coredata.PeopleKindContractor: "CONTRACTOR", } ) -func (ec *executionContext) unmarshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus(ctx context.Context, v any) (*coredata.PolicyStatus, error) { +func (ec *executionContext) unmarshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus(ctx context.Context, v any) (*coredata.PolicyStatus, error) { if v == nil { return nil, nil } tmp, err := graphql.UnmarshalString(v) - res := unmarshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus[tmp] + res := unmarshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus[tmp] return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus(ctx context.Context, sel ast.SelectionSet, v *coredata.PolicyStatus) graphql.Marshaler { +func (ec *executionContext) marshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus(ctx context.Context, sel ast.SelectionSet, v *coredata.PolicyStatus) graphql.Marshaler { if v == nil { return graphql.Null } - res := graphql.MarshalString(marshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus[*v]) + res := graphql.MarshalString(marshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus[*v]) return res } var ( - unmarshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus = map[string]coredata.PolicyStatus{ + unmarshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus = map[string]coredata.PolicyStatus{ "DRAFT": coredata.PolicyStatusDraft, "ACTIVE": coredata.PolicyStatusActive, } - marshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPolicyStatus = map[coredata.PolicyStatus]string{ + marshalOPolicyStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPolicyStatus = map[coredata.PolicyStatus]string{ coredata.PolicyStatusDraft: "DRAFT", coredata.PolicyStatusActive: "ACTIVE", } ) -func (ec *executionContext) unmarshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier(ctx context.Context, v any) (*coredata.RiskTier, error) { +func (ec *executionContext) unmarshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier(ctx context.Context, v any) (*coredata.RiskTier, error) { if v == nil { return nil, nil } tmp, err := graphql.UnmarshalString(v) - res := unmarshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier[tmp] + res := unmarshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier[tmp] return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier(ctx context.Context, sel ast.SelectionSet, v *coredata.RiskTier) graphql.Marshaler { +func (ec *executionContext) marshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier(ctx context.Context, sel ast.SelectionSet, v *coredata.RiskTier) graphql.Marshaler { if v == nil { return graphql.Null } - res := graphql.MarshalString(marshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier[*v]) + res := graphql.MarshalString(marshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier[*v]) return res } var ( - unmarshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier = map[string]coredata.RiskTier{ + unmarshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier = map[string]coredata.RiskTier{ "CRITICAL": coredata.RiskTierCritical, "SIGNIFICANT": coredata.RiskTierSignificant, "GENERAL": coredata.RiskTierGeneral, } - marshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐRiskTier = map[coredata.RiskTier]string{ + marshalORiskTier2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTier = map[coredata.RiskTier]string{ coredata.RiskTierCritical: "CRITICAL", coredata.RiskTierSignificant: "SIGNIFICANT", coredata.RiskTierGeneral: "GENERAL", } ) -func (ec *executionContext) unmarshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality(ctx context.Context, v any) (*coredata.ServiceCriticality, error) { +func (ec *executionContext) unmarshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality(ctx context.Context, v any) (*coredata.ServiceCriticality, error) { if v == nil { return nil, nil } tmp, err := graphql.UnmarshalString(v) - res := unmarshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality[tmp] + res := unmarshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality[tmp] return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality(ctx context.Context, sel ast.SelectionSet, v *coredata.ServiceCriticality) graphql.Marshaler { +func (ec *executionContext) marshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality(ctx context.Context, sel ast.SelectionSet, v *coredata.ServiceCriticality) graphql.Marshaler { if v == nil { return graphql.Null } - res := graphql.MarshalString(marshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality[*v]) + res := graphql.MarshalString(marshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality[*v]) return res } var ( - unmarshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality = map[string]coredata.ServiceCriticality{ + unmarshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality = map[string]coredata.ServiceCriticality{ "LOW": coredata.ServiceCriticalityLow, "MEDIUM": coredata.ServiceCriticalityMedium, "HIGH": coredata.ServiceCriticalityHigh, } - marshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐServiceCriticality = map[coredata.ServiceCriticality]string{ + marshalOServiceCriticality2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐServiceCriticality = map[coredata.ServiceCriticality]string{ coredata.ServiceCriticalityLow: "LOW", coredata.ServiceCriticalityMedium: "MEDIUM", coredata.ServiceCriticalityHigh: "HIGH", @@ -18179,29 +18159,29 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return res } -func (ec *executionContext) unmarshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState(ctx context.Context, v any) (*coredata.TaskState, error) { +func (ec *executionContext) unmarshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState(ctx context.Context, v any) (*coredata.TaskState, error) { if v == nil { return nil, nil } tmp, err := graphql.UnmarshalString(v) - res := unmarshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState[tmp] + res := unmarshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState[tmp] return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState(ctx context.Context, sel ast.SelectionSet, v *coredata.TaskState) graphql.Marshaler { +func (ec *executionContext) marshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState(ctx context.Context, sel ast.SelectionSet, v *coredata.TaskState) graphql.Marshaler { if v == nil { return graphql.Null } - res := graphql.MarshalString(marshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState[*v]) + res := graphql.MarshalString(marshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState[*v]) return res } var ( - unmarshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState = map[string]coredata.TaskState{ + unmarshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState = map[string]coredata.TaskState{ "TODO": coredata.TaskStateTodo, "DONE": coredata.TaskStateDone, } - marshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState = map[coredata.TaskState]string{ + marshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTaskState = map[coredata.TaskState]string{ coredata.TaskStateTodo: "TODO", coredata.TaskStateDone: "DONE", } diff --git a/pkg/server/api/console/v1/sign_in_handler.go b/pkg/server/api/console/v1/sign_in_handler.go new file mode 100644 index 000000000..c8512b1de --- /dev/null +++ b/pkg/server/api/console/v1/sign_in_handler.go @@ -0,0 +1,92 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package console_v1 + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "github.com/getprobo/probo/pkg/gid" + "github.com/getprobo/probo/pkg/securecookie" + "github.com/getprobo/probo/pkg/usrmgr" + "go.gearno.de/kit/httpserver" +) + +type ( + SignInRequest struct { + Email string `json:"email"` + Password string `json:"password"` + } + + SignInResponse struct { + User UserResponse `json:"user"` + } + + UserResponse struct { + ID gid.GID `json:"id"` + Email string `json:"email"` + FullName string `json:"fullName"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } +) + +func SignInHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + var req SignInRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err)) + return + } + + user, session, err := usrmgrSvc.SignIn(r.Context(), req.Email, req.Password) + if err != nil { + var ErrInvalidCredentials *usrmgr.ErrInvalidCredentials + if errors.As(err, &ErrInvalidCredentials) { + httpserver.RenderError(w, http.StatusUnauthorized, err) + return + } + + panic(fmt.Errorf("cannot sign in: %w", err)) + } + + securecookie.Set( + w, + securecookie.DefaultConfig( + authCfg.CookieName, + authCfg.CookieSecret, + ), + session.ID.String(), + ) + + httpserver.RenderJSON( + w, + http.StatusOK, + SignInResponse{ + User: UserResponse{ + ID: user.ID, + Email: user.EmailAddress, + FullName: user.FullName, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + }, + }, + ) + } +} diff --git a/pkg/server/api/console/v1/sign_out_handler.go b/pkg/server/api/console/v1/sign_out_handler.go new file mode 100644 index 000000000..90400fa29 --- /dev/null +++ b/pkg/server/api/console/v1/sign_out_handler.go @@ -0,0 +1,57 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package console_v1 + +import ( + "fmt" + "net/http" + + "github.com/getprobo/probo/pkg/gid" + "github.com/getprobo/probo/pkg/securecookie" + "github.com/getprobo/probo/pkg/usrmgr" + "go.gearno.de/kit/httpserver" +) + +func SignOutHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + sessionID, err := securecookie.Get(r, securecookie.DefaultConfig( + authCfg.CookieName, + authCfg.CookieSecret, + )) + if err != nil { + httpserver.RenderError(w, http.StatusBadRequest, err) + return + } + + gid, err := gid.ParseGID(sessionID) + if err != nil { + httpserver.RenderError(w, http.StatusBadRequest, err) + return + } + + err = usrmgrSvc.SignOut(r.Context(), gid) + if err != nil { + panic(fmt.Errorf("cannot sign out: %w", err)) + } + + securecookie.Clear(w, securecookie.DefaultConfig( + authCfg.CookieName, + authCfg.CookieSecret, + )) + + httpserver.RenderJSON(w, http.StatusOK, map[string]bool{"success": true}) + } +} diff --git a/pkg/server/api/console/v1/sign_up_handler.go b/pkg/server/api/console/v1/sign_up_handler.go new file mode 100644 index 000000000..690b65eaa --- /dev/null +++ b/pkg/server/api/console/v1/sign_up_handler.go @@ -0,0 +1,88 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package console_v1 + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/securecookie" + "github.com/getprobo/probo/pkg/usrmgr" + "go.gearno.de/kit/httpserver" +) + +type ( + SignUpRequest struct { + Email string `json:"email"` + Password string `json:"password"` + FullName string `json:"fullName"` + } + + SignUpResponse struct { + User UserResponse `json:"user"` + } +) + +func SignUpHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req SignUpRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err)) + return + } + + user, session, err := usrmgrSvc.SignUp( + r.Context(), + req.Email, + req.Password, + req.FullName, + ) + if err != nil { + var errUserAlreadyExists *coredata.ErrUserAlreadyExists + if errors.As(err, &errUserAlreadyExists) { + httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot register user: %w", err)) + return + } + + panic(fmt.Errorf("cannot register user: %w", err)) + } + + securecookie.Set( + w, + securecookie.DefaultConfig( + authCfg.CookieName, + authCfg.CookieSecret, + ), + session.ID.String(), + ) + + httpserver.RenderJSON( + w, + http.StatusOK, + SignUpResponse{ + User: UserResponse{ + ID: user.ID, + Email: user.EmailAddress, + FullName: user.FullName, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + }, + }, + ) + } +} diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index f41530dd1..38d0ca321 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -6,9 +6,9 @@ import ( "time" "github.com/99designs/gqlgen/graphql" + "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" - "github.com/getprobo/probo/pkg/coredata" ) type Node interface { diff --git a/pkg/usrmgr/usrmgr.go b/pkg/usrmgr/usrmgr.go index 76e4581e9..5e2e43e8e 100644 --- a/pkg/usrmgr/usrmgr.go +++ b/pkg/usrmgr/usrmgr.go @@ -16,7 +16,9 @@ package usrmgr import ( "context" + "errors" "fmt" + "strings" "time" "github.com/getprobo/probo/pkg/coredata" @@ -32,16 +34,22 @@ type ( hp *passwdhash.Profile } - RegisterUserParams struct { - Email string - Password string - FullName string - } - ErrInvalidCredentials struct { message string } + ErrInvalidEmail struct { + email string + } + + ErrInvalidPassword struct { + length int + } + + ErrInvalidFullName struct { + fullName string + } + ErrUserAlreadyExists struct { message string } @@ -71,78 +79,93 @@ func (e ErrSessionExpired) Error() string { return e.message } +func (e ErrInvalidEmail) Error() string { + return fmt.Sprintf("invalid email: %s", e.email) +} + +func (e ErrInvalidPassword) Error() string { + return fmt.Sprintf("invalid password: the length must be at least %d characters", e.length) +} + +func (e ErrInvalidFullName) Error() string { + return fmt.Sprintf("invalid full name: %s", e.fullName) +} + func NewService( ctx context.Context, pgClient *pg.Client, - pepper []byte, + hp *passwdhash.Profile, ) (*Service, error) { - hp, err := passwdhash.NewProfile(pepper) - if err != nil { - return nil, fmt.Errorf("cannot create hashing profile: %w", err) - } - return &Service{ pg: pgClient, hp: hp, }, nil } -func (s Service) RegisterUser( +func (s Service) SignUp( ctx context.Context, - params RegisterUserParams, -) (*coredata.User, error) { - if params.Email == "" || params.Password == "" || params.FullName == "" { - return nil, fmt.Errorf("email, password, and full name are required") + email, password, fullName string, +) (*coredata.User, *coredata.Session, error) { + if !strings.Contains(email, "@") { + return nil, nil, &ErrInvalidEmail{email} } - // Use a high iteration count for password hashing - const iterations = 600000 - hashedPassword, err := s.hp.HashPassword([]byte(params.Password), iterations) + if len(password) < 8 { + return nil, nil, &ErrInvalidPassword{len(password)} + } + + if fullName == "" { + return nil, nil, &ErrInvalidFullName{fullName} + } + + hashedPassword, err := s.hp.HashPassword([]byte(password)) if err != nil { - return nil, fmt.Errorf("cannot hash password: %w", err) + return nil, nil, fmt.Errorf("cannot hash password: %w", err) } now := time.Now() user := &coredata.User{ ID: gid.New(gid.NilTenant, coredata.UserEntityType), - EmailAddress: params.Email, + EmailAddress: email, HashedPassword: hashedPassword, - FullName: params.FullName, + FullName: fullName, CreatedAt: now, UpdatedAt: now, } + session := &coredata.Session{ + ID: gid.New(gid.NilTenant, coredata.SessionEntityType), + UserID: user.ID, + CreatedAt: now, + UpdatedAt: now, + } + err = s.pg.WithTx( ctx, func(tx pg.Conn) error { - // Check if user already exists - existingUser := &coredata.User{} - err := existingUser.LoadByEmail(ctx, tx, params.Email) - if err == nil { - return &ErrUserAlreadyExists{message: "user with this email already exists"} - } - - // Insert the new user if err := user.Insert(ctx, tx); err != nil { return fmt.Errorf("cannot insert user: %w", err) } + if err := session.Insert(ctx, tx); err != nil { + return fmt.Errorf("cannot insert session: %w", err) + } + return nil }, ) if err != nil { - return nil, err + return nil, nil, err } - return user, nil + return user, session, nil } -func (s Service) Login( +func (s Service) SignIn( ctx context.Context, - email string, - password string, -) (*coredata.Session, error) { + email, password string, +) (*coredata.User, *coredata.Session, error) { now := time.Now() user := &coredata.User{} session := &coredata.Session{ @@ -157,7 +180,13 @@ func (s Service) Login( ctx, func(tx pg.Conn) error { if err := user.LoadByEmail(ctx, tx, email); err != nil { - return &ErrInvalidCredentials{message: "invalid email or password"} + var errUserNotFound *coredata.ErrUserNotFound + + if errors.As(err, &errUserNotFound) { + return &ErrInvalidCredentials{message: "invalid email or password"} + } + + return fmt.Errorf("cannot load user by email: %w", err) } ok, err := s.hp.ComparePasswordAndHash([]byte(password), user.HashedPassword) @@ -169,7 +198,6 @@ func (s Service) Login( return &ErrInvalidCredentials{message: "invalid email or password"} } - // Set the user ID in the session session.UserID = user.ID if err := session.Insert(ctx, tx); err != nil { @@ -181,20 +209,25 @@ func (s Service) Login( ) if err != nil { - return nil, err + return nil, nil, err } - return session, nil + return user, session, nil } -func (s Service) Logout( +func (s Service) SignOut( ctx context.Context, sessionID gid.GID, ) error { - return s.pg.WithTx( + return s.pg.WithConn( ctx, func(tx pg.Conn) error { - return coredata.DeleteSession(ctx, tx, sessionID) + err := coredata.DeleteSession(ctx, tx, sessionID) + if err != nil { + return fmt.Errorf("cannot delete session: %w", err) + } + + return nil }, ) }