Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
11 changes: 5 additions & 6 deletions go/apps/api/routes/v2_apis_list_keys/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,12 +303,11 @@ func (h *Handler) buildKeyResponseData(keyData *db.KeyData, plaintext string) (o
ExternalId: keyData.Identity.ExternalID,
}

if len(keyData.Identity.Meta) > 0 {
var identityMeta map[string]any
_ = json.Unmarshal(keyData.Identity.Meta, &identityMeta) // Ignore error, default to nil
if identityMeta != nil {
response.Identity.Meta = &identityMeta
}
identityMeta, err := db.UnmarshalNullableJSONTo[map[string]any](keyData.Identity.Meta)
if err != nil {
h.Logger.Error("failed to unmarshal identity meta", "error", err)
} else {
response.Identity.Meta = &identityMeta
}
}

Expand Down
12 changes: 4 additions & 8 deletions go/apps/api/routes/v2_identities_create_identity/200_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,7 @@ func TestCreateIdentitySuccessfully(t *testing.T) {
require.NoError(t, err)
require.Equal(t, identity.ExternalID, req.ExternalId)

var dbMeta map[string]any
err = json.Unmarshal(identity.Meta, &dbMeta)
dbMeta, err := db.UnmarshalNullableJSONTo[map[string]any](identity.Meta)
require.NoError(t, err)
require.Equal(t, *meta, dbMeta)
})
Expand Down Expand Up @@ -239,8 +238,7 @@ func TestCreateIdentitySuccessfully(t *testing.T) {
require.Equal(t, identity.ExternalID, req.ExternalId)

// Verify metadata
var dbMeta map[string]any
err = json.Unmarshal(identity.Meta, &dbMeta)
dbMeta, err := db.UnmarshalNullableJSONTo[map[string]any](identity.Meta)
require.NoError(t, err)
require.Equal(t, *meta, dbMeta)

Expand Down Expand Up @@ -315,8 +313,7 @@ func TestCreateIdentitySuccessfully(t *testing.T) {
require.Equal(t, identity.ExternalID, req.ExternalId)

// Verify complex metadata is correctly stored and retrieved
var dbMeta map[string]any
err = json.Unmarshal(identity.Meta, &dbMeta)
dbMeta, err := db.UnmarshalNullableJSONTo[map[string]any](identity.Meta)
require.NoError(t, err)

// Convert expected and actual to JSON strings for comparison to handle potential subtle differences in map types
Expand Down Expand Up @@ -425,8 +422,7 @@ func TestCreateIdentitySuccessfully(t *testing.T) {
require.NoError(t, err)

// Verify metadata
var dbMeta map[string]any
err = json.Unmarshal(identity.Meta, &dbMeta)
dbMeta, err := db.UnmarshalNullableJSONTo[map[string]any](identity.Meta)
require.NoError(t, err)

// Convert to JSON for comparison
Expand Down
17 changes: 6 additions & 11 deletions go/apps/api/routes/v2_identities_get_identity/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,12 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
return err
}

// Parse metadata
var metaMap map[string]any
if len(identity.Meta) > 0 {
err = json.Unmarshal(identity.Meta, &metaMap)
if err != nil {
return fault.Wrap(err,
fault.Internal("unable to unmarshal metadata"), fault.Public("We're unable to parse the identity's metadata."),
)
}
} else {
metaMap = make(map[string]any)
metaMap, err := db.UnmarshalNullableJSONTo[map[string]any](identity.Meta)
if err != nil {
return fault.Wrap(err,
fault.Internal("unable to unmarshal metadata"),
fault.Public("We're unable to parse the identity's metadata."),
)
}

// Format ratelimits for the response
Expand Down
27 changes: 11 additions & 16 deletions go/apps/api/routes/v2_identities_list_identities/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package handler
import (
"context"
"database/sql"
"encoding/json"
"errors"
"net/http"

Expand All @@ -17,8 +16,10 @@ import (
"github.com/unkeyed/unkey/go/pkg/zen"
)

type Request = openapi.V2IdentitiesListIdentitiesRequestBody
type Response = openapi.V2IdentitiesListIdentitiesResponseBody
type (
Request = openapi.V2IdentitiesListIdentitiesRequestBody
Response = openapi.V2IdentitiesListIdentitiesResponseBody
)

// Handler implements zen.Route interface for the v2 identities list identities endpoint
type Handler struct {
Expand Down Expand Up @@ -62,7 +63,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
IDCursor: cursor,
Limit: int32(limit + 1), // nolint:gosec
})

if err != nil {
return fault.Wrap(err,
fault.Internal("unable to list identities"), fault.Public("We're unable to list the identities."),
Expand Down Expand Up @@ -136,19 +136,14 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
}

// Add metadata if available
if len(identity.Meta) > 0 {
// Initialize the Meta field with an empty map
metaMap := make(map[string]interface{})
newIdentity.Meta = &metaMap

// Unmarshal the identity metadata into the map
err = json.Unmarshal(identity.Meta, &metaMap)
if err != nil {
return fault.Wrap(err,
fault.Internal("unable to unmarshal identity metadata"), fault.Public("We're unable to parse the metadata for the identity."),
)
}
metaMap, err := db.UnmarshalNullableJSONTo[map[string]any](identity.Meta)
if err != nil {
return fault.Wrap(err,
fault.Internal("unable to unmarshal identity metadata"),
fault.Public("We're unable to parse the metadata for the identity."),
)
}
newIdentity.Meta = &metaMap

// Append the identity to the results
data = append(data, newIdentity)
Expand Down
12 changes: 4 additions & 8 deletions go/apps/api/routes/v2_keys_get_key/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,20 +158,16 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
}
}

// Set identity
if keyData.Identity != nil {
response.Identity = &openapi.Identity{
Id: keyData.Identity.ID,
ExternalId: keyData.Identity.ExternalID,
}

if len(keyData.Identity.Meta) > 0 {
var identityMeta map[string]any
if err := json.Unmarshal(keyData.Identity.Meta, &identityMeta); err != nil {
h.Logger.Error("failed to unmarshal identity meta", "error", err)
} else {
response.Identity.Meta = &identityMeta
}
if identityMeta, err := db.UnmarshalNullableJSONTo[map[string]any](keyData.Identity.Meta); err != nil {
h.Logger.Error("failed to unmarshal identity meta", "error", err)
} else {
response.Identity.Meta = &identityMeta
}
}

Expand Down
20 changes: 10 additions & 10 deletions go/apps/api/routes/v2_keys_verify_key/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (
"github.com/unkeyed/unkey/go/pkg/zen"
)

type Request = openapi.V2KeysVerifyKeyRequestBody
type Response = openapi.V2KeysVerifyKeyResponseBody
type (
Request = openapi.V2KeysVerifyKeyRequestBody
Response = openapi.V2KeysVerifyKeyResponseBody
)

const DefaultCost = 1

Expand Down Expand Up @@ -227,14 +229,12 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
keyData.Identity.Ratelimits = ptr.P(identityRatelimits)
}

if len(key.Key.IdentityMeta) > 0 {
err = json.Unmarshal(key.Key.IdentityMeta, &keyData.Identity.Meta)
if err != nil {
return fault.Wrap(err, fault.Code(codes.App.Internal.UnexpectedError.URN()),
fault.Internal("unable to unmarshal identity meta"),
fault.Public("We encountered an error while trying to unmarshal the identity meta data."),
)
}
keyData.Identity.Meta, err = db.UnmarshalNullableJSONTo[*map[string]any](key.Key.IdentityMeta)
if err != nil {
return fault.Wrap(err, fault.Code(codes.App.Internal.UnexpectedError.URN()),
fault.Internal("unable to unmarshal identity meta"),
fault.Public("We encountered an error while trying to unmarshal the identity meta data."),
)
}
}

Expand Down
12 changes: 4 additions & 8 deletions go/apps/api/routes/v2_keys_whoami/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,16 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
}
}

// Set identity
if keyData.Identity != nil {
response.Identity = &openapi.Identity{
Id: keyData.Identity.ID,
ExternalId: keyData.Identity.ExternalID,
}

if len(keyData.Identity.Meta) > 0 {
var identityMeta map[string]any
if err := json.Unmarshal(keyData.Identity.Meta, &identityMeta); err != nil {
h.Logger.Error("failed to unmarshal identity meta", "error", err)
} else {
response.Identity.Meta = &identityMeta
}
if identityMeta, err := db.UnmarshalNullableJSONTo[map[string]any](keyData.Identity.Meta); err != nil {
h.Logger.Error("failed to unmarshal identity meta", "error", err)
} else {
response.Identity.Meta = &identityMeta
}
}

Expand Down
65 changes: 64 additions & 1 deletion go/pkg/db/custom_types.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package db

import (
"encoding/json"

dbtype "github.com/unkeyed/unkey/go/pkg/db/types"
)

// These types mirror the database models and support JSON serialization and deserialization.
// They are used to unmarshal aggregated results (e.g., JSON arrays) returned by database queries.

type RoleInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Expand All @@ -29,3 +30,65 @@ type RatelimitInfo struct {
Duration int64 `json:"duration"`
AutoApply bool `json:"auto_apply"`
}

// UnmarshalJSONArrayTo deserializes a JSON byte array into a slice of type T.
// It handles the common pattern of database queries returning aggregated JSON arrays
// that need to be unmarshaled into Go structs.
//
// Returns an empty slice if:
// - data is nil
// - data is not []byte
// - the byte slice is empty
// - JSON unmarshaling fails
//
// This fail-safe behavior prevents nil pointer panics and simplifies error handling
// at call sites, allowing callers to always work with a valid slice.
//
// Example usage:
//
// roles := UnmarshalJSONArrayTo[RoleInfo](row.Roles)
// // roles is guaranteed to be []RoleInfo, never nil
func UnmarshalJSONArrayTo[T any](data any) []T {
if data == nil {
return []T{}
}

bytes, ok := data.([]byte)
if !ok || len(bytes) == 0 {
return []T{}
}

var result []T
if err := json.Unmarshal(bytes, &result); err != nil {
return []T{}
}
return result
}

// UnmarshalNullableJSONTo unmarshals the JSON data and returns the result with an error.
// Returns zero value and nil if the JSON is NULL or invalid.
// Returns zero value and error if JSON unmarshaling fails.
//
// Use this when you want inline assignment with generic type inference:
//
// myData, err := UnmarshalNullableJSONTo[MyStruct](nullJSON)
// if err != nil {
// return err
// }
// // use myData
func UnmarshalNullableJSONTo[T any](data []byte) (T, error) {
var result T

// NULL check
if data == nil {
return result, nil
}

// Empty check
if len(data) == 0 {
return result, nil
}

err := json.Unmarshal(data, &result)
return result, err
}
49 changes: 10 additions & 39 deletions go/pkg/db/key_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package db

import (
"database/sql"
"encoding/json"
)

// KeyData represents the complete data for a key including all relationships
Expand Down Expand Up @@ -82,11 +81,11 @@ func buildKeyDataFromKeyAuth(r *ListLiveKeysByKeyAuthIDRow) *KeyData {
Workspace: Workspace{}, // Empty Workspace since not in this query
EncryptedKey: r.EncryptedKey,
EncryptionKeyID: r.EncryptionKeyID,
Roles: nil,
Permissions: nil,
RolePermissions: nil,
Ratelimits: nil,
} //nolint:exhaustruct
Roles: UnmarshalJSONArrayTo[RoleInfo](r.Roles),
Permissions: UnmarshalJSONArrayTo[PermissionInfo](r.Permissions),
RolePermissions: UnmarshalJSONArrayTo[PermissionInfo](r.RolePermissions),
Ratelimits: UnmarshalJSONArrayTo[RatelimitInfo](r.Ratelimits),
}

if r.IdentityID.Valid {
kd.Identity = &Identity{
Expand All @@ -97,20 +96,6 @@ func buildKeyDataFromKeyAuth(r *ListLiveKeysByKeyAuthIDRow) *KeyData {
}
}

// It's fine to fail here
if roleBytes, ok := r.Roles.([]byte); ok && roleBytes != nil {
_ = json.Unmarshal(roleBytes, &kd.Roles) // Ignore error, default to empty array
}
if permissionsBytes, ok := r.Permissions.([]byte); ok && permissionsBytes != nil {
_ = json.Unmarshal(permissionsBytes, &kd.Permissions) // Ignore error, default to empty array
}
if rolePermissionsBytes, ok := r.RolePermissions.([]byte); ok && rolePermissionsBytes != nil {
_ = json.Unmarshal(rolePermissionsBytes, &kd.RolePermissions) // Ignore error, default to empty array
}
if ratelimitsBytes, ok := r.Ratelimits.([]byte); ok && ratelimitsBytes != nil {
_ = json.Unmarshal(ratelimitsBytes, &kd.Ratelimits) // Ignore error, default to empty array
}

return kd
}

Expand Down Expand Up @@ -146,11 +131,11 @@ func buildKeyData(r *FindLiveKeyByHashRow) *KeyData {
Workspace: r.Workspace,
EncryptedKey: r.EncryptedKey,
EncryptionKeyID: r.EncryptionKeyID,
Roles: nil,
Permissions: nil,
RolePermissions: nil,
Ratelimits: nil,
} //nolint:exhaustruct
Roles: UnmarshalJSONArrayTo[RoleInfo](r.Roles),
Permissions: UnmarshalJSONArrayTo[PermissionInfo](r.Permissions),
RolePermissions: UnmarshalJSONArrayTo[PermissionInfo](r.RolePermissions),
Ratelimits: UnmarshalJSONArrayTo[RatelimitInfo](r.Ratelimits),
}

if r.IdentityTableID.Valid {
kd.Identity = &Identity{
Expand All @@ -161,19 +146,5 @@ func buildKeyData(r *FindLiveKeyByHashRow) *KeyData {
}
}

// It's fine to fail here
if roleBytes, ok := r.Roles.([]byte); ok && roleBytes != nil {
_ = json.Unmarshal(roleBytes, &kd.Roles) // Ignore error, default to empty array
}
if permissionsBytes, ok := r.Permissions.([]byte); ok && permissionsBytes != nil {
_ = json.Unmarshal(permissionsBytes, &kd.Permissions) // Ignore error, default to empty array
}
if rolePermissionsBytes, ok := r.RolePermissions.([]byte); ok && rolePermissionsBytes != nil {
_ = json.Unmarshal(rolePermissionsBytes, &kd.RolePermissions) // Ignore error, default to empty array
}
if ratelimitsBytes, ok := r.Ratelimits.([]byte); ok && ratelimitsBytes != nil {
_ = json.Unmarshal(ratelimitsBytes, &kd.Ratelimits) // Ignore error, default to empty array
}

return kd
}
Loading