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
1 change: 0 additions & 1 deletion apps/agent/pkg/api/routes/openapi/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
)

func New(svc routes.Services) *routes.Route {

return routes.NewRoute("GET", "/openapi.json",
func(w http.ResponseWriter, r *http.Request) {

Expand Down
1 change: 0 additions & 1 deletion apps/agent/services/vault/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ type Config struct {
}

func New(cfg Config) (*Service, error) {

encryptionKey, decryptionKeys, err := loadMasterKeys(cfg.MasterKeys)
if err != nil {
return nil, fmt.Errorf("unable to load master keys: %w", err)
Expand Down
581 changes: 209 additions & 372 deletions go/apps/api/openapi/gen.go

Large diffs are not rendered by default.

818 changes: 71 additions & 747 deletions go/apps/api/openapi/openapi.yaml

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions go/apps/api/routes/openapi/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package handler

import (
"context"

"github.com/unkeyed/unkey/go/apps/api/openapi"
"github.com/unkeyed/unkey/go/pkg/otel/logging"
"github.com/unkeyed/unkey/go/pkg/zen"
)

// Handler implements zen.Route interface for the API reference endpoint
type Handler struct {
// Services as public fields (even though not used in this handler, showing the pattern)
Logger logging.Logger
}

// Method returns the HTTP method this route responds to
func (h *Handler) Method() string {
return "GET"
}

// Path returns the URL path pattern this route matches
func (h *Handler) Path() string {
return "/openapi.yaml"
}

// Handle processes the HTTP request
func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
s.AddHeader("Content-Type", "text/html")
return s.Send(200, openapi.Spec)
}
24 changes: 24 additions & 0 deletions go/apps/api/routes/register.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package routes

import (
openapi "github.com/unkeyed/unkey/go/apps/api/routes/openapi"
"github.com/unkeyed/unkey/go/apps/api/routes/reference"
v2Liveness "github.com/unkeyed/unkey/go/apps/api/routes/v2_liveness"

Expand Down Expand Up @@ -33,6 +34,7 @@ import (
v2KeysAddPermissions "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_add_permissions"
v2KeysAddRoles "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_add_roles"
v2KeysCreateKey "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_create_key"
v2KeysGetKey "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_get_key"
v2KeysRemovePermissions "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_remove_permissions"
v2KeysRemoveRoles "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_remove_roles"
v2KeysSetPermissions "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_set_permissions"
Expand Down Expand Up @@ -344,6 +346,20 @@ func Register(srv *zen.Server, svc *Services) {
Keys: svc.Keys,
Permissions: svc.Permissions,
Auditlogs: svc.Auditlogs,
Vault: svc.Vault,
},
)

// v2/keys.getKey
srv.RegisterRoute(
defaultMiddlewares,
&v2KeysGetKey.Handler{
Logger: svc.Logger,
DB: svc.Database,
Keys: svc.Keys,
Permissions: svc.Permissions,
Auditlogs: svc.Auditlogs,
Vault: svc.Vault,
},
)

Expand Down Expand Up @@ -429,5 +445,13 @@ func Register(srv *zen.Server, svc *Services) {
}, &reference.Handler{
Logger: svc.Logger,
})
srv.RegisterRoute([]zen.Middleware{
withTracing,
withMetrics,
withLogging,
withErrorHandling,
}, &openapi.Handler{
Logger: svc.Logger,
})

}
5 changes: 1 addition & 4 deletions go/apps/api/routes/v2_apis_create_api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
},
})
if err != nil {
return "", fault.Wrap(err,
fault.Code(codes.App.Internal.ServiceUnavailable.URN()),
fault.Internal("database failed to insert audit logs"), fault.Public("Failed to insert audit logs"),
)
return "", err
}

return apiId, nil
Expand Down
5 changes: 1 addition & 4 deletions go/apps/api/routes/v2_apis_delete_api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
UserAgent: s.UserAgent(),
}})
if err != nil {
return fault.Wrap(err,
fault.Code(codes.App.Internal.ServiceUnavailable.URN()),
fault.Internal("audit log error"), fault.Public("Failed to create audit log for API deletion."),
)
return err
}

return nil
Expand Down
1 change: 1 addition & 0 deletions go/apps/api/routes/v2_apis_get_api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
fault.Internal("database error"), fault.Public("Failed to retrieve API information."),
)
}

// Check if API belongs to the authorized workspace
if api.WorkspaceID != auth.AuthorizedWorkspaceID {
return fault.New("wrong workspace",
Expand Down
62 changes: 58 additions & 4 deletions go/apps/api/routes/v2_apis_list_keys/200_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import (
handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_list_keys"
"github.com/unkeyed/unkey/go/pkg/db"
"github.com/unkeyed/unkey/go/pkg/hash"
"github.com/unkeyed/unkey/go/pkg/ptr"
"github.com/unkeyed/unkey/go/pkg/testutil"
"github.com/unkeyed/unkey/go/pkg/uid"

vaultv1 "github.com/unkeyed/unkey/go/gen/proto/vault/v1"
)

func TestSuccess(t *testing.T) {
Expand All @@ -35,10 +38,10 @@ func TestSuccess(t *testing.T) {
workspace := h.Resources().UserWorkspace

// Create a root key with appropriate permissions
rootKey := h.CreateRootKey(workspace.ID, "api.*.read_key", "api.*.read_api")
rootKey := h.CreateRootKey(workspace.ID, "api.*.read_key", "api.*.read_api", "api.*.decrypt_key")

// Create a keyAuth (keyring) for the API
keyAuthID := uid.New("keyauth")
keyAuthID := uid.New(uid.KeyAuthPrefix)
err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{
ID: keyAuthID,
WorkspaceID: workspace.ID,
Expand All @@ -48,6 +51,12 @@ func TestSuccess(t *testing.T) {
})
require.NoError(t, err)

err = db.Query.UpdateKeyringKeyEncryption(ctx, h.DB.RW(), db.UpdateKeyringKeyEncryptionParams{
ID: keyAuthID,
StoreEncryptedKeys: true,
})
require.NoError(t, err)

// Create a test API
apiID := uid.New("api")
err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{
Expand Down Expand Up @@ -85,6 +94,7 @@ func TestSuccess(t *testing.T) {
})
require.NoError(t, err)

encryptedKeysMap := make(map[string]struct{})
// Create test keys with various configurations
testKeys := []struct {
id string
Expand Down Expand Up @@ -139,10 +149,12 @@ func TestSuccess(t *testing.T) {
metaBytes, _ = json.Marshal(keyData.meta)
}

key := keyData.start + uid.New("")

insertParams := db.InsertKeyParams{
ID: keyData.id,
KeyringID: keyAuthID,
Hash: hash.Sha256(keyData.start + uid.New("")),
Hash: hash.Sha256(key),
Start: keyData.start,
WorkspaceID: workspace.ID,
ForWorkspaceID: sql.NullString{Valid: false},
Expand All @@ -166,6 +178,22 @@ func TestSuccess(t *testing.T) {

err := db.Query.InsertKey(ctx, h.DB.RW(), insertParams)
require.NoError(t, err)

encryption, err := h.Vault.Encrypt(ctx, &vaultv1.EncryptRequest{
Keyring: h.Resources().UserWorkspace.ID,
Data: key,
})
require.NoError(t, err)

err = db.Query.InsertKeyEncryption(ctx, h.DB.RW(), db.InsertKeyEncryptionParams{
WorkspaceID: h.Resources().UserWorkspace.ID,
KeyID: keyData.id,
CreatedAt: time.Now().UnixMilli(),
Encrypted: encryption.GetEncrypted(),
EncryptionKeyID: encryption.GetKeyId(),
})
require.NoError(t, err)
encryptedKeysMap[keyData.id] = struct{}{}
}

// Set up request headers
Expand Down Expand Up @@ -407,7 +435,7 @@ func TestSuccess(t *testing.T) {

t.Run("empty API returns empty result", func(t *testing.T) {
// Create a new API with no keys
emptyKeyAuthID := uid.New("keyauth")
emptyKeyAuthID := uid.New(uid.KeyAuthPrefix)
err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{
ID: emptyKeyAuthID,
WorkspaceID: workspace.ID,
Expand Down Expand Up @@ -570,4 +598,30 @@ func TestSuccess(t *testing.T) {
require.True(t, foundKeyWithRatelimits, "Should find the key with ratelimits in response")
require.True(t, foundKeyWithoutRatelimits, "Should find the key without ratelimits in response")
})

t.Run("verify encrypted key is returned correctly", func(t *testing.T) {
req := handler.Request{
ApiId: apiID,
Decrypt: ptr.P(true),
}

res := testutil.CallRoute[handler.Request, handler.Response](
h,
route,
headers,
req,
)

require.Equal(t, 200, res.Status)
require.NotNil(t, res.Body.Data)

for _, key := range res.Body.Data {
_, exists := encryptedKeysMap[key.KeyId]
if !exists {
continue
}

require.NotEmpty(t, ptr.SafeDeref(key.Plaintext), "Key should be decrypted and have plaintext")
}
})
}
8 changes: 7 additions & 1 deletion go/apps/api/routes/v2_apis_list_keys/403_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestAuthorizationErrors(t *testing.T) {
workspace := h.Resources().UserWorkspace

// Create a keyAuth (keyring) for the API
keyAuthID := uid.New("keyauth")
keyAuthID := uid.New(uid.KeyAuthPrefix)
err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{
ID: keyAuthID,
WorkspaceID: workspace.ID,
Expand All @@ -44,6 +44,12 @@ func TestAuthorizationErrors(t *testing.T) {
})
require.NoError(t, err)

err = db.Query.UpdateKeyringKeyEncryption(ctx, h.DB.RW(), db.UpdateKeyringKeyEncryptionParams{
ID: keyAuthID,
StoreEncryptedKeys: true,
})
require.NoError(t, err)

// Create a test API
apiID := uid.New("api")
err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{
Expand Down
6 changes: 3 additions & 3 deletions go/apps/api/routes/v2_apis_list_keys/404_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func TestNotFoundErrors(t *testing.T) {
// Test case for API in different workspace
t.Run("API in different workspace", func(t *testing.T) {
// Create a keyAuth for the API in the different workspace
otherKeyAuthID := uid.New("keyauth")
otherKeyAuthID := uid.New(uid.KeyAuthPrefix)
err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{
ID: otherKeyAuthID,
WorkspaceID: workspace2.ID,
Expand Down Expand Up @@ -105,7 +105,7 @@ func TestNotFoundErrors(t *testing.T) {
// Test case for deleted API
t.Run("deleted API", func(t *testing.T) {
// Create a keyAuth for the API
deletedKeyAuthID := uid.New("keyauth")
deletedKeyAuthID := uid.New(uid.KeyAuthPrefix)
err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{
ID: deletedKeyAuthID,
WorkspaceID: workspace1.ID,
Expand Down Expand Up @@ -244,7 +244,7 @@ func TestNotFoundErrors(t *testing.T) {
// Test case for API that exists but has no keys (should return 200 with empty array)
t.Run("API exists but has no keys", func(t *testing.T) {
// Create a keyAuth for the API
emptyKeyAuthID := uid.New("keyauth")
emptyKeyAuthID := uid.New(uid.KeyAuthPrefix)
err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{
ID: emptyKeyAuthID,
WorkspaceID: workspace1.ID,
Expand Down
83 changes: 83 additions & 0 deletions go/apps/api/routes/v2_apis_list_keys/412_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package handler_test

import (
"context"
"database/sql"
"fmt"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/require"
"github.com/unkeyed/unkey/go/apps/api/openapi"
handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_list_keys"
"github.com/unkeyed/unkey/go/pkg/db"
"github.com/unkeyed/unkey/go/pkg/ptr"
"github.com/unkeyed/unkey/go/pkg/testutil"
"github.com/unkeyed/unkey/go/pkg/uid"
)

func TestPreconditionError(t *testing.T) {
ctx := context.Background()
h := testutil.NewHarness(t)

route := &handler.Handler{
Logger: h.Logger,
DB: h.DB,
Keys: h.Keys,
Permissions: h.Permissions,
Vault: h.Vault,
}

h.Register(route)

// Create API manually
keyAuthID := uid.New(uid.KeyAuthPrefix)
err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{
ID: keyAuthID,
WorkspaceID: h.Resources().UserWorkspace.ID,
CreatedAtM: time.Now().UnixMilli(),
DefaultPrefix: sql.NullString{Valid: false, String: ""},
DefaultBytes: sql.NullInt32{Valid: false, Int32: 0},
})
require.NoError(t, err)

apiID := uid.New(uid.APIPrefix)
err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{
ID: apiID,
Name: "test-api",
WorkspaceID: h.Resources().UserWorkspace.ID,
AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey},
KeyAuthID: sql.NullString{Valid: true, String: keyAuthID},
CreatedAtM: time.Now().UnixMilli(),
})
require.NoError(t, err)

// Create a root key with appropriate permissions
rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.read_key", "api.*.read_api", "api.*.decrypt_key")

// Set up request headers
headers := http.Header{
"Content-Type": {"application/json"},
"Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
}

// Test case for API ID with special characters
t.Run("Try decrypting key without opt-in", func(t *testing.T) {
req := handler.Request{
ApiId: apiID,
Decrypt: ptr.P(true),
}

res := testutil.CallRoute[handler.Request, openapi.PreconditionFailedErrorResponse](
h,
route,
headers,
req,
)

require.Equal(t, 412, res.Status)
require.NotNil(t, res.Body)
require.NotNil(t, res.Body.Error)
})
}
Loading
Loading