Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions store/postgres/clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package postgres

import (
"maps"

"github.com/meigma/authkit"
"github.com/meigma/authkit/oidc"
)

// cloneAttributes returns an independent copy of attrs, or nil for an empty
// input.
func cloneAttributes(attrs map[string]any) map[string]any {
if len(attrs) == 0 {
return nil
}

cloned := make(map[string]any, len(attrs))
maps.Copy(cloned, attrs)

return cloned
}

// cloneStrings returns an independent copy of values, or nil for an empty
// input.
func cloneStrings(values []string) []string {
if len(values) == 0 {
return nil
}

cloned := make([]string, len(values))
copy(cloned, values)

return cloned
}

// cloneClaimPath returns an independent copy of path.
func cloneClaimPath(path authkit.ClaimPath) authkit.ClaimPath {
if len(path) == 0 {
return nil
}

cloned := make(authkit.ClaimPath, len(path))
copy(cloned, path)

return cloned
}

// cloneClaimPaths returns an independent copy of paths with every inner
// ClaimPath copied as well.
func cloneClaimPaths(paths []authkit.ClaimPath) []authkit.ClaimPath {
if len(paths) == 0 {
return nil
}

cloned := make([]authkit.ClaimPath, len(paths))
for i, path := range paths {
cloned[i] = cloneClaimPath(path)
}

return cloned
}

// cloneProvider returns a copy of provider with independent Audiences,
// SupportedSigningAlgorithms, and ForwardedClaims slices.
func cloneProvider(provider oidc.Provider) oidc.Provider {
provider.Audiences = cloneStrings(provider.Audiences)
provider.SupportedSigningAlgorithms = cloneStrings(provider.SupportedSigningAlgorithms)
provider.ForwardedClaims = cloneClaimPaths(provider.ForwardedClaims)

return provider
}

// cloneProvisioningRule returns a copy of rule with an independent
// AssignRoleIDs slice.
func cloneProvisioningRule(rule authkit.ProvisioningRule) authkit.ProvisioningRule {
rule.AssignRoleIDs = cloneStrings(rule.AssignRoleIDs)

return rule
}
94 changes: 94 additions & 0 deletions store/postgres/codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package postgres

import (
"encoding/json"
"fmt"

"github.com/go-webauthn/webauthn/webauthn"

"github.com/meigma/authkit"
)

// encodeAttributes returns the JSON encoding of attrs for storage as a JSONB
// column. An empty or nil map encodes to the empty string so call sites can
// pass the result through nullif(... , empty string) and cast to jsonb to
// normalize NULL.
func encodeAttributes(attrs map[string]any) (string, error) {
if len(attrs) == 0 {
return "", nil
}

encoded, err := json.Marshal(attrs)
if err != nil {
return "", fmt.Errorf("postgres: encode principal attributes: %w", err)
}

return string(encoded), nil
}

// decodeAttributes parses a principal-attributes JSONB column into a map.
// Empty, "null", or `{}` payloads return nil so principals with no
// attributes carry no allocated map.
func decodeAttributes(encoded string) (map[string]any, error) {
if encoded == "" || encoded == "null" {
//nolint:nilnil // Nil attributes are the normalized zero value for principals.
return nil, nil
}

var attrs map[string]any
if err := json.Unmarshal([]byte(encoded), &attrs); err != nil {
return nil, fmt.Errorf("postgres: decode principal attributes: %w", err)
}
if len(attrs) == 0 {
//nolint:nilnil // Nil attributes are the normalized zero value for principals.
return nil, nil
}

return attrs, nil
}

// encodeClaimPaths returns the JSON encoding of paths for storage as a JSONB
// column. A nil or empty input encodes to the literal "[]" so the column is
// never NULL.
func encodeClaimPaths(paths []authkit.ClaimPath) (string, error) {
if len(paths) == 0 {
return "[]", nil
}

encoded, err := json.Marshal(paths)
if err != nil {
return "", fmt.Errorf("postgres: encode claim paths: %w", err)
}

return string(encoded), nil
}

// decodeClaimPaths parses a JSONB-encoded slice of claim paths. Empty or
// "null" payloads return nil. The returned slice is independent of any
// internal buffer.
func decodeClaimPaths(encoded string) ([]authkit.ClaimPath, error) {
if encoded == "" || encoded == "null" {
return nil, nil
}

var paths []authkit.ClaimPath
if err := json.Unmarshal([]byte(encoded), &paths); err != nil {
return nil, fmt.Errorf("postgres: decode claim paths: %w", err)
}
if len(paths) == 0 {
return nil, nil
}

return cloneClaimPaths(paths), nil
}

// encodeWebAuthnCredential returns the JSON encoding of credential for
// storage as a JSONB column. The full upstream record is preserved.
func encodeWebAuthnCredential(credential webauthn.Credential) (string, error) {
encoded, err := json.Marshal(credential)
if err != nil {
return "", fmt.Errorf("postgres: encode passkey credential: %w", err)
}

return string(encoded), nil
}
17 changes: 17 additions & 0 deletions store/postgres/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package postgres

import (
"errors"

"github.com/jackc/pgx/v5/pgconn"
)

// isPostgresCode reports whether err is a pgx-wrapped PostgreSQL error with
// the supplied sqlstate code. Use with the package-level violation
// constants (`uniqueViolation`, `foreignKeyViolation`) rather than passing
// raw sqlstate strings at call sites.
func isPostgresCode(err error, code string) bool {
var pgErr *pgconn.PgError

return errors.As(err, &pgErr) && pgErr.Code == code
}
Loading