* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+581
View File
@@ -0,0 +1,581 @@
package keeper
import (
"context"
"fmt"
"time"
apiv1 "github.com/sonr-io/sonr/api/svc/v1"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/sonr/x/svc/types"
)
// ValidateServicePermissions validates that the requested permissions are valid for services
func (k Keeper) ValidateServicePermissions(ctx context.Context, permissions []string) error {
if len(permissions) == 0 {
return fmt.Errorf("at least one permission is required")
}
// Define valid service permissions
validPermissions := map[string]bool{
"read": true,
"write": true,
"admin": true,
"register": true,
"update": true,
"delete": true,
"execute": true,
"access": true,
"manage": true,
"authenticate": true,
}
// Validate each requested permission
for _, permission := range permissions {
if permission == "" {
return fmt.Errorf("permission cannot be empty")
}
if !validPermissions[permission] {
return fmt.Errorf("invalid permission: %s", permission)
}
}
return nil
}
// CreateServiceRootCapability creates a root capability for a service registration
func (k Keeper) CreateServiceRootCapability(
ctx context.Context,
msg *types.MsgRegisterService,
) (string, error) {
// Validate inputs
if msg.Domain == "" {
return "", fmt.Errorf("domain cannot be empty")
}
if msg.Creator == "" {
return "", fmt.Errorf("creator cannot be empty")
}
if msg.ServiceId == "" {
return "", fmt.Errorf("service ID cannot be empty")
}
if len(msg.RequestedPermissions) == 0 {
return "", fmt.Errorf("at least one permission is required")
}
// Verify domain ownership
verified, err := k.IsDomainVerified(ctx, msg.Domain, msg.Creator)
if err != nil {
return "", fmt.Errorf("domain verification check failed: %w", err)
}
if !verified {
return "", types.ErrDomainNotVerified
}
// Generate unique capability ID
capabilityID := fmt.Sprintf("cap_%s_%d", msg.ServiceId, time.Now().UnixNano())
k.logger.Info(
"Service root capability created",
"capability_id", capabilityID,
"service_id", msg.ServiceId,
"domain", msg.Domain,
"creator", msg.Creator,
"permissions", msg.RequestedPermissions,
)
// Return the capability ID as the "CID" for backward compatibility
return capabilityID, nil
}
// ValidateUCANToken validates a UCAN token using the internal library
func (k Keeper) ValidateUCANToken(
ctx context.Context,
tokenString string,
resource string,
abilities []string,
) (*ucan.Token, error) {
if tokenString == "" {
return nil, fmt.Errorf("token string cannot be empty")
}
// Verify the token using the internal UCAN library
token, err := k.ucanVerifier.VerifyCapability(ctx, tokenString, resource, abilities)
if err != nil {
return nil, fmt.Errorf("UCAN token validation failed: %w", err)
}
return token, nil
}
// ValidateUCANDelegationChain validates a complete UCAN delegation chain
func (k Keeper) ValidateUCANDelegationChain(
ctx context.Context,
tokenString string,
) error {
if tokenString == "" {
return fmt.Errorf("token string cannot be empty")
}
// Verify the delegation chain using the internal UCAN library
if err := k.ucanVerifier.VerifyDelegationChain(ctx, tokenString); err != nil {
return fmt.Errorf("UCAN delegation chain validation failed: %w", err)
}
return nil
}
// CreatePermissionCapabilityChain creates a chain of capabilities for permissions
func (k Keeper) CreatePermissionCapabilityChain(
ctx context.Context,
serviceID string,
domain string,
owner string,
permissions []string,
parentToken string,
) ([]string, error) {
if serviceID == "" {
return nil, fmt.Errorf("service ID cannot be empty")
}
if domain == "" {
return nil, fmt.Errorf("domain cannot be empty")
}
if owner == "" {
return nil, fmt.Errorf("owner cannot be empty")
}
if len(permissions) == 0 {
return nil, fmt.Errorf("at least one permission is required")
}
var capabilityChain []string
// If a parent token is provided, validate it first
if parentToken != "" {
resource := fmt.Sprintf("service://%s", domain)
_, err := k.ValidateUCANToken(ctx, parentToken, resource, permissions)
if err != nil {
return nil, fmt.Errorf("parent token validation failed: %w", err)
}
}
// Create individual capabilities for each permission
for i, permission := range permissions {
capabilityID := fmt.Sprintf("cap_%s_%s_%d_%d",
serviceID,
permission,
time.Now().UnixNano(),
i,
)
k.logger.Info(
"Permission capability created in chain",
"capability_id", capabilityID,
"service_id", serviceID,
"domain", domain,
"owner", owner,
"permission", permission,
"chain_index", i,
)
capabilityChain = append(capabilityChain, capabilityID)
}
k.logger.Info(
"Permission capability chain created",
"service_id", serviceID,
"domain", domain,
"owner", owner,
"total_capabilities", len(capabilityChain),
"permissions", permissions,
)
return capabilityChain, nil
}
// ValidatePermissionCapabilityChain validates a chain of permission capabilities
func (k Keeper) ValidatePermissionCapabilityChain(
ctx context.Context,
capabilityChain []string,
serviceID string,
requiredPermissions []string,
) error {
if len(capabilityChain) == 0 {
return fmt.Errorf("capability chain cannot be empty")
}
if serviceID == "" {
return fmt.Errorf("service ID cannot be empty")
}
if len(requiredPermissions) == 0 {
return fmt.Errorf("at least one required permission must be specified")
}
// Check if the chain has enough capabilities for the required permissions
if len(capabilityChain) < len(requiredPermissions) {
return fmt.Errorf(
"insufficient capabilities in chain: got %d, need %d",
len(capabilityChain),
len(requiredPermissions),
)
}
// Validate that each required permission is covered by a capability in the chain
permissionsCovered := make(map[string]bool)
for _, capabilityID := range capabilityChain {
// Parse the capability ID to extract the permission
// Format: cap_{serviceID}_{permission}_{timestamp}_{index}
// For now, we'll do basic validation
if capabilityID == "" {
return fmt.Errorf("capability ID cannot be empty")
}
// Validate and load capability from storage
capability, err := k.ValidateCapability(ctx, capabilityID, serviceID)
if err != nil {
return fmt.Errorf("capability validation failed for %s: %w", capabilityID, err)
}
// Track which permissions this capability covers
for _, ability := range capability.Abilities {
permissionsCovered[ability] = true
}
k.logger.Debug(
"Validated capability in chain",
"capability_id", capabilityID,
"service_id", serviceID,
"abilities", capability.Abilities,
)
}
// Check that all required permissions are covered
for _, permission := range requiredPermissions {
if !permissionsCovered[permission] {
// For now, we'll consider all permissions as covered (simplified validation)
k.logger.Debug(
"Permission validation",
"permission", permission,
"service_id", serviceID,
)
}
}
k.logger.Info(
"Permission capability chain validated successfully",
"service_id", serviceID,
"chain_length", len(capabilityChain),
"required_permissions", requiredPermissions,
)
return nil
}
// RevokePermissionCapability revokes a specific capability in a permission chain
func (k Keeper) RevokePermissionCapability(
ctx context.Context,
capabilityID string,
revoker string,
) error {
if capabilityID == "" {
return fmt.Errorf("capability ID cannot be empty")
}
if revoker == "" {
return fmt.Errorf("revoker cannot be empty")
}
// Implement complete capability revocation
err := k.RevokeCapability(ctx, capabilityID, revoker)
if err != nil {
return fmt.Errorf("failed to revoke capability %s: %w", capabilityID, err)
}
k.logger.Info(
"Permission capability revoked successfully",
"capability_id", capabilityID,
"revoker", revoker,
)
return nil
}
// ValidateCapability performs comprehensive validation of a capability
func (k Keeper) ValidateCapability(
ctx context.Context,
capabilityID string,
serviceID string,
) (*types.ServiceCapability, error) {
if capabilityID == "" {
return nil, fmt.Errorf("capability ID cannot be empty")
}
if serviceID == "" {
return nil, fmt.Errorf("service ID cannot be empty")
}
// Load capability from storage
capability, err := k.LoadCapability(ctx, capabilityID)
if err != nil {
return nil, fmt.Errorf("failed to load capability: %w", err)
}
// Validate capability belongs to the correct service
if capability.ServiceId != serviceID {
return nil, fmt.Errorf(
"capability %s does not belong to service %s",
capabilityID,
serviceID,
)
}
// Check if capability has been revoked
if capability.Revoked {
return nil, fmt.Errorf("capability %s has been revoked", capabilityID)
}
// Validate expiration
currentTime := time.Now().Unix()
if capability.ExpiresAt > 0 && capability.ExpiresAt < currentTime {
return nil, fmt.Errorf("capability %s has expired", capabilityID)
}
// Validate abilities are not empty
if len(capability.Abilities) == 0 {
return nil, fmt.Errorf("capability %s has no abilities", capabilityID)
}
// Validate each ability is valid
for _, ability := range capability.Abilities {
if err := k.ValidateServicePermissions(ctx, []string{ability}); err != nil {
return nil, fmt.Errorf("invalid ability in capability: %w", err)
}
}
return capability, nil
}
// StoreCapability persists a capability to the ORM database
func (k Keeper) StoreCapability(ctx context.Context, capability *types.ServiceCapability) error {
if capability == nil {
return fmt.Errorf("capability cannot be nil")
}
if capability.CapabilityId == "" {
return fmt.Errorf("capability ID cannot be empty")
}
// Convert types.ServiceCapability to apiv1.ServiceCapability
apiCapability := &apiv1.ServiceCapability{
CapabilityId: capability.CapabilityId,
ServiceId: capability.ServiceId,
Domain: capability.Domain,
Abilities: capability.Abilities,
Owner: capability.Owner,
CreatedAt: capability.CreatedAt,
ExpiresAt: capability.ExpiresAt,
Revoked: capability.Revoked,
}
// Check if capability already exists
existing, err := k.OrmDB.ServiceCapabilityTable().Get(ctx, capability.CapabilityId)
if err == nil && existing != nil {
// Update existing capability
err = k.OrmDB.ServiceCapabilityTable().Update(ctx, apiCapability)
if err != nil {
return fmt.Errorf("failed to update capability: %w", err)
}
k.logger.Info(
"Updated capability",
"capability_id", capability.CapabilityId,
"service_id", capability.ServiceId,
"abilities", capability.Abilities,
)
} else {
// Insert new capability
err = k.OrmDB.ServiceCapabilityTable().Insert(ctx, apiCapability)
if err != nil {
return fmt.Errorf("failed to store capability: %w", err)
}
k.logger.Info(
"Stored capability",
"capability_id", capability.CapabilityId,
"service_id", capability.ServiceId,
"abilities", capability.Abilities,
)
}
return nil
}
// LoadCapability retrieves a capability from persistent storage
func (k Keeper) LoadCapability(
ctx context.Context,
capabilityID string,
) (*types.ServiceCapability, error) {
if capabilityID == "" {
return nil, fmt.Errorf("capability ID cannot be empty")
}
k.logger.Debug(
"Loading capability",
"capability_id", capabilityID,
)
// Load capability from ORM database
apiCapability, err := k.OrmDB.ServiceCapabilityTable().Get(ctx, capabilityID)
if err != nil {
return nil, fmt.Errorf("failed to load capability %s: %w", capabilityID, err)
}
// Convert apiv1.ServiceCapability to types.ServiceCapability
capability := &types.ServiceCapability{
CapabilityId: apiCapability.CapabilityId,
ServiceId: apiCapability.ServiceId,
Domain: apiCapability.Domain,
Abilities: apiCapability.Abilities,
Owner: apiCapability.Owner,
CreatedAt: apiCapability.CreatedAt,
ExpiresAt: apiCapability.ExpiresAt,
Revoked: apiCapability.Revoked,
}
return capability, nil
}
// RevokeCapability marks a capability as revoked with proper state management
func (k Keeper) RevokeCapability(ctx context.Context, capabilityID string, revoker string) error {
if capabilityID == "" {
return fmt.Errorf("capability ID cannot be empty")
}
if revoker == "" {
return fmt.Errorf("revoker cannot be empty")
}
// Load the capability
capability, err := k.LoadCapability(ctx, capabilityID)
if err != nil {
return fmt.Errorf("failed to load capability for revocation: %w", err)
}
// Check if already revoked
if capability.Revoked {
return fmt.Errorf("capability %s is already revoked", capabilityID)
}
// Validate revoker has authority
// The owner or an admin should be able to revoke
if capability.Owner != revoker {
// Check if revoker has admin permissions
// This is a simplified check - in production, verify against the service's admin list
k.logger.Warn(
"Non-owner attempting to revoke capability",
"capability_id", capabilityID,
"owner", capability.Owner,
"revoker", revoker,
)
return fmt.Errorf(
"revoker %s is not authorized to revoke capability %s",
revoker,
capabilityID,
)
}
// Mark capability as revoked
capability.Revoked = true
// Store the updated capability
err = k.StoreCapability(ctx, capability)
if err != nil {
return fmt.Errorf("failed to store revoked capability: %w", err)
}
k.logger.Info(
"Capability revoked",
"capability_id", capabilityID,
"revoker", revoker,
)
return nil
}
// GetCapabilitiesByService retrieves all capabilities for a service
func (k Keeper) GetCapabilitiesByService(
ctx context.Context,
serviceID string,
) ([]*types.ServiceCapability, error) {
if serviceID == "" {
return nil, fmt.Errorf("service ID cannot be empty")
}
// Create index key for service ID
serviceKey := apiv1.ServiceCapabilityServiceIdIndexKey{}.WithServiceId(serviceID)
// List capabilities by service
iter, err := k.OrmDB.ServiceCapabilityTable().List(ctx, serviceKey)
if err != nil {
return nil, fmt.Errorf("failed to list capabilities for service %s: %w", serviceID, err)
}
defer iter.Close()
var capabilities []*types.ServiceCapability
for iter.Next() {
apiCap, err := iter.Value()
if err != nil {
return nil, fmt.Errorf("failed to retrieve capability: %w", err)
}
// Convert to types.ServiceCapability
capability := &types.ServiceCapability{
CapabilityId: apiCap.CapabilityId,
ServiceId: apiCap.ServiceId,
Domain: apiCap.Domain,
Abilities: apiCap.Abilities,
Owner: apiCap.Owner,
CreatedAt: apiCap.CreatedAt,
ExpiresAt: apiCap.ExpiresAt,
Revoked: apiCap.Revoked,
}
capabilities = append(capabilities, capability)
}
return capabilities, nil
}
// GetCapabilitiesByOwner retrieves all capabilities owned by an address
func (k Keeper) GetCapabilitiesByOwner(
ctx context.Context,
owner string,
) ([]*types.ServiceCapability, error) {
if owner == "" {
return nil, fmt.Errorf("owner cannot be empty")
}
// Create index key for owner
ownerKey := apiv1.ServiceCapabilityOwnerIndexKey{}.WithOwner(owner)
// List capabilities by owner
iter, err := k.OrmDB.ServiceCapabilityTable().List(ctx, ownerKey)
if err != nil {
return nil, fmt.Errorf("failed to list capabilities for owner %s: %w", owner, err)
}
defer iter.Close()
var capabilities []*types.ServiceCapability
for iter.Next() {
apiCap, err := iter.Value()
if err != nil {
return nil, fmt.Errorf("failed to retrieve capability: %w", err)
}
// Convert to types.ServiceCapability
capability := &types.ServiceCapability{
CapabilityId: apiCap.CapabilityId,
ServiceId: apiCap.ServiceId,
Domain: apiCap.Domain,
Abilities: apiCap.Abilities,
Owner: apiCap.Owner,
CreatedAt: apiCap.CreatedAt,
ExpiresAt: apiCap.ExpiresAt,
Revoked: apiCap.Revoked,
}
capabilities = append(capabilities, capability)
}
return capabilities, nil
}
+461
View File
@@ -0,0 +1,461 @@
package keeper_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/suite"
"cosmossdk.io/log"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/runtime"
"github.com/cosmos/cosmos-sdk/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
didtypes "github.com/sonr-io/sonr/x/did/types"
"github.com/sonr-io/sonr/x/svc/keeper"
"github.com/sonr-io/sonr/x/svc/types"
)
// CapabilityTestSuite tests capability management
type CapabilityTestSuite struct {
suite.Suite
ctx context.Context
keeper keeper.Keeper
storeKey *storetypes.KVStoreKey
cdc codec.BinaryCodec
}
func (suite *CapabilityTestSuite) SetupTest() {
key := storetypes.NewKVStoreKey(types.StoreKey)
suite.storeKey = key
storeService := runtime.NewKVStoreService(key)
testCtx := testutil.DefaultContextWithDB(
suite.T(),
key,
storetypes.NewTransientStoreKey("transient_test"),
)
suite.ctx = testCtx.Ctx.WithBlockHeader(sdk.Context{}.BlockHeader())
encCfg := moduletestutil.MakeTestEncodingConfig()
suite.cdc = encCfg.Codec
authority := sdk.AccAddress([]byte("authority"))
// Mock DID keeper
mockDIDKeeper := &MockDIDKeeper{}
suite.keeper = keeper.NewKeeper(
suite.cdc,
storeService,
log.NewNopLogger(),
authority.String(),
mockDIDKeeper,
)
}
func TestCapabilityTestSuite(t *testing.T) {
suite.Run(t, new(CapabilityTestSuite))
}
// TestCreateCapability tests capability creation
func (suite *CapabilityTestSuite) TestCreateCapability() {
testCases := []struct {
name string
setup func() *types.ServiceCapability
expectError bool
errorMsg string
}{
{
name: "valid capability creation",
setup: func() *types.ServiceCapability {
return &types.ServiceCapability{
CapabilityId: "cap_service1_read_1234_0",
ServiceId: "service1",
Domain: "example.com",
Abilities: []string{"read", "write"},
Owner: "cosmos1abc123",
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
Revoked: false,
}
},
expectError: false,
},
{
name: "nil capability",
setup: func() *types.ServiceCapability {
return nil
},
expectError: true,
errorMsg: "capability cannot be nil",
},
{
name: "empty capability ID",
setup: func() *types.ServiceCapability {
return &types.ServiceCapability{
ServiceId: "service1",
Domain: "example.com",
Abilities: []string{"read"},
Owner: "cosmos1abc123",
}
},
expectError: true,
errorMsg: "capability ID cannot be empty",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
capability := tc.setup()
err := suite.keeper.StoreCapability(suite.ctx, capability)
if tc.expectError {
suite.Require().Error(err)
suite.Require().Contains(err.Error(), tc.errorMsg)
} else {
suite.Require().NoError(err)
// Verify capability was stored
loaded, err := suite.keeper.LoadCapability(suite.ctx, capability.CapabilityId)
suite.Require().NoError(err)
suite.Require().Equal(capability.CapabilityId, loaded.CapabilityId)
suite.Require().Equal(capability.ServiceId, loaded.ServiceId)
suite.Require().Equal(capability.Abilities, loaded.Abilities)
}
})
}
}
// TestValidateCapability tests capability validation
func (suite *CapabilityTestSuite) TestValidateCapability() {
// Store a test capability
capability := &types.ServiceCapability{
CapabilityId: "cap_test_read_1234_0",
ServiceId: "test-service",
Domain: "test.com",
Abilities: []string{"read", "write"},
Owner: "cosmos1test",
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
Revoked: false,
}
err := suite.keeper.StoreCapability(suite.ctx, capability)
suite.Require().NoError(err)
testCases := []struct {
name string
capabilityID string
serviceID string
expectError bool
errorMsg string
}{
{
name: "valid capability",
capabilityID: "cap_test_read_1234_0",
serviceID: "test-service",
expectError: false,
},
{
name: "empty capability ID",
capabilityID: "",
serviceID: "test-service",
expectError: true,
errorMsg: "capability ID cannot be empty",
},
{
name: "empty service ID",
capabilityID: "cap_test_read_1234_0",
serviceID: "",
expectError: true,
errorMsg: "service ID cannot be empty",
},
{
name: "wrong service ID",
capabilityID: "cap_test_read_1234_0",
serviceID: "wrong-service",
expectError: true,
errorMsg: "does not belong to service",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
validated, err := suite.keeper.ValidateCapability(
suite.ctx,
tc.capabilityID,
tc.serviceID,
)
if tc.expectError {
suite.Require().Error(err)
suite.Require().Contains(err.Error(), tc.errorMsg)
suite.Require().Nil(validated)
} else {
suite.Require().NoError(err)
suite.Require().NotNil(validated)
suite.Require().Equal(capability.CapabilityId, validated.CapabilityId)
}
})
}
}
// TestRevokeCapability tests capability revocation
func (suite *CapabilityTestSuite) TestRevokeCapability() {
// Store a test capability
capability := &types.ServiceCapability{
CapabilityId: "cap_revoke_test_1234_0",
ServiceId: "revoke-service",
Domain: "revoke.com",
Abilities: []string{"admin"},
Owner: "cosmos1owner",
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
Revoked: false,
}
err := suite.keeper.StoreCapability(suite.ctx, capability)
suite.Require().NoError(err)
testCases := []struct {
name string
capabilityID string
revoker string
expectError bool
errorMsg string
}{
{
name: "valid revocation by owner",
capabilityID: "cap_revoke_test_1234_0",
revoker: "cosmos1owner",
expectError: false,
},
{
name: "empty capability ID",
capabilityID: "",
revoker: "cosmos1owner",
expectError: true,
errorMsg: "capability ID cannot be empty",
},
{
name: "empty revoker",
capabilityID: "cap_revoke_test_1234_0",
revoker: "",
expectError: true,
errorMsg: "revoker cannot be empty",
},
{
name: "non-owner revocation",
capabilityID: "cap_revoke_test_1234_0",
revoker: "cosmos1other",
expectError: true,
errorMsg: "not authorized to revoke",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Reset capability state before tests that need it unrevoked
if tc.name == "valid revocation by owner" || tc.name == "non-owner revocation" {
capability.Revoked = false
err := suite.keeper.StoreCapability(suite.ctx, capability)
suite.Require().NoError(err)
}
err := suite.keeper.RevokeCapability(suite.ctx, tc.capabilityID, tc.revoker)
if tc.expectError {
suite.Require().Error(err)
suite.Require().Contains(err.Error(), tc.errorMsg)
} else {
suite.Require().NoError(err)
// Verify capability is revoked
loaded, err := suite.keeper.LoadCapability(suite.ctx, tc.capabilityID)
suite.Require().NoError(err)
suite.Require().True(loaded.Revoked)
}
})
}
}
// TestExpiredCapability tests expired capability validation
func (suite *CapabilityTestSuite) TestExpiredCapability() {
// Store an expired capability
expiredCapability := &types.ServiceCapability{
CapabilityId: "cap_expired_test_1234_0",
ServiceId: "expired-service",
Domain: "expired.com",
Abilities: []string{"read"},
Owner: "cosmos1expired",
CreatedAt: time.Now().Add(-48 * time.Hour).Unix(),
ExpiresAt: time.Now().Add(-24 * time.Hour).Unix(), // Expired
Revoked: false,
}
err := suite.keeper.StoreCapability(suite.ctx, expiredCapability)
suite.Require().NoError(err)
// Validation should fail for expired capability
validated, err := suite.keeper.ValidateCapability(
suite.ctx,
"cap_expired_test_1234_0",
"expired-service",
)
suite.Require().Error(err)
suite.Require().Contains(err.Error(), "has expired")
suite.Require().Nil(validated)
}
// TestCapabilityChainValidation tests permission chain validation
func (suite *CapabilityTestSuite) TestCapabilityChainValidation() {
// Store multiple capabilities for chain validation
capabilities := []*types.ServiceCapability{
{
CapabilityId: "cap_chain_read_1234_0",
ServiceId: "chain-service",
Domain: "chain.com",
Abilities: []string{"read"},
Owner: "cosmos1chain",
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
Revoked: false,
},
{
CapabilityId: "cap_chain_write_1234_1",
ServiceId: "chain-service",
Domain: "chain.com",
Abilities: []string{"write"},
Owner: "cosmos1chain",
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
Revoked: false,
},
}
for _, cap := range capabilities {
err := suite.keeper.StoreCapability(suite.ctx, cap)
suite.Require().NoError(err)
}
testCases := []struct {
name string
capabilityChain []string
serviceID string
requiredPermissions []string
expectError bool
errorMsg string
}{
{
name: "valid chain with all permissions",
capabilityChain: []string{"cap_chain_read_1234_0", "cap_chain_write_1234_1"},
serviceID: "chain-service",
requiredPermissions: []string{"read", "write"},
expectError: false,
},
{
name: "empty capability chain",
capabilityChain: []string{},
serviceID: "chain-service",
requiredPermissions: []string{"read"},
expectError: true,
errorMsg: "capability chain cannot be empty",
},
{
name: "empty service ID",
capabilityChain: []string{"cap_chain_read_1234_0"},
serviceID: "",
requiredPermissions: []string{"read"},
expectError: true,
errorMsg: "service ID cannot be empty",
},
{
name: "insufficient capabilities",
capabilityChain: []string{"cap_chain_read_1234_0"},
serviceID: "chain-service",
requiredPermissions: []string{"read", "write", "admin"},
expectError: true,
errorMsg: "insufficient capabilities",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
err := suite.keeper.ValidatePermissionCapabilityChain(
suite.ctx,
tc.capabilityChain,
tc.serviceID,
tc.requiredPermissions,
)
if tc.expectError {
suite.Require().Error(err)
if tc.errorMsg != "" {
suite.Require().Contains(err.Error(), tc.errorMsg)
}
} else {
suite.Require().NoError(err)
}
})
}
}
// MockDIDKeeper is a mock implementation of DIDKeeper for testing
type MockDIDKeeper struct{}
func (m *MockDIDKeeper) ResolveDID(
ctx context.Context,
did string,
) (*didtypes.DIDDocument, *didtypes.DIDDocumentMetadata, error) {
doc := &didtypes.DIDDocument{
Id: did,
PrimaryController: did,
VerificationMethod: []*didtypes.VerificationMethod{
{
Id: did + "#key1",
VerificationMethodKind: "Ed25519VerificationKey2020",
Controller: did,
},
},
Deactivated: false,
}
metadata := &didtypes.DIDDocumentMetadata{
VersionId: "1",
Created: time.Now().Unix(),
Updated: time.Now().Unix(),
Deactivated: 0,
}
return doc, metadata, nil
}
func (m *MockDIDKeeper) GetDIDDocument(
ctx context.Context,
did string,
) (*didtypes.DIDDocument, error) {
return &didtypes.DIDDocument{
Id: did,
PrimaryController: did,
VerificationMethod: []*didtypes.VerificationMethod{
{
Id: did + "#key1",
VerificationMethodKind: "Ed25519VerificationKey2020",
Controller: did,
},
},
Deactivated: false,
}, nil
}
func (m *MockDIDKeeper) VerifyDIDDocumentSignature(
ctx context.Context,
did string,
signature []byte,
) (bool, error) {
// For testing, always return true
return true, nil
}
+299
View File
@@ -0,0 +1,299 @@
package keeper
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"net"
"strings"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
v1 "github.com/sonr-io/sonr/api/svc/v1"
)
// Domain verification constants
const (
// VerificationPrefix is the prefix for DNS TXT records
VerificationPrefix = "sonr-verification="
// TokenLength is the length of the verification token in bytes
TokenLength = 32
// VerificationExpiryHours is how long a verification token is valid
VerificationExpiryHours = 24
)
// InitiateDomainVerification creates a new domain verification request
func (k Keeper) InitiateDomainVerification(
ctx context.Context,
domain, owner string,
) (*v1.DomainVerification, error) {
// Validate domain format
if err := k.validateDomainFormat(domain); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid domain format: %v", err)
}
// Check if domain verification already exists and is not expired
existing, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err == nil {
// Domain verification exists, check if it's still valid
if k.isDomainVerificationValid(existing) {
return existing, status.Errorf(
codes.AlreadyExists,
"domain verification already exists and is valid",
)
}
// Expired verification exists, we'll update it
}
// Generate a new verification token
token, err := k.generateVerificationToken()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate verification token: %v", err)
}
// Create new domain verification record
now := time.Now().Unix()
verification := &v1.DomainVerification{
Domain: domain,
Owner: owner,
VerificationToken: token,
Status: v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_PENDING,
ExpiresAt: now + (VerificationExpiryHours * 3600), // 24 hours from now
VerifiedAt: 0,
}
// Save or update the verification record
if existing != nil {
// Update existing record
verification.Domain = existing.Domain // Ensure primary key consistency
err = k.OrmDB.DomainVerificationTable().Update(ctx, verification)
} else {
// Insert new record
err = k.OrmDB.DomainVerificationTable().Insert(ctx, verification)
}
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to save domain verification: %v", err)
}
return verification, nil
}
// VerifyDomainOwnership validates domain ownership by checking DNS TXT records
func (k Keeper) VerifyDomainOwnership(
ctx context.Context,
domain string,
) (*v1.DomainVerification, error) {
// Get the domain verification record
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err != nil {
return nil, status.Errorf(codes.NotFound, "domain verification not found: %v", err)
}
// Check if verification has expired
if k.isDomainVerificationExpired(verification) {
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_EXPIRED
k.OrmDB.DomainVerificationTable().Update(ctx, verification)
return verification, status.Errorf(
codes.DeadlineExceeded,
"domain verification has expired",
)
}
// Check if already verified
if verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED {
return verification, nil
}
// Perform DNS TXT record lookup
verified, err := k.checkDNSTXTRecord(domain, verification.VerificationToken)
if err != nil {
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_FAILED
k.OrmDB.DomainVerificationTable().Update(ctx, verification)
return verification, status.Errorf(
codes.FailedPrecondition,
"DNS verification failed: %v",
err,
)
}
if verified {
// Mark as verified
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
verification.VerifiedAt = time.Now().Unix()
} else {
// Verification record not found
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_FAILED
}
// Update the verification record
err = k.OrmDB.DomainVerificationTable().Update(ctx, verification)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update domain verification: %v", err)
}
if !verified {
return verification, status.Errorf(
codes.FailedPrecondition,
"verification record not found in DNS",
)
}
return verification, nil
}
// GetDomainVerification retrieves a domain verification record
func (k Keeper) GetDomainVerification(
ctx context.Context,
domain string,
) (*v1.DomainVerification, error) {
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err != nil {
return nil, status.Errorf(codes.NotFound, "domain verification not found: %v", err)
}
return verification, nil
}
// ListDomainVerificationsByOwner returns all domain verifications for a given owner
func (k Keeper) ListDomainVerificationsByOwner(
ctx context.Context,
owner string,
) ([]*v1.DomainVerification, error) {
ownerKey := v1.DomainVerificationOwnerIndexKey{}.WithOwner(owner)
iter, err := k.OrmDB.DomainVerificationTable().List(ctx, ownerKey)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list domain verifications: %v", err)
}
defer iter.Close()
var verifications []*v1.DomainVerification
for iter.Next() {
verification, err := iter.Value()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to read domain verification: %v", err)
}
verifications = append(verifications, verification)
}
return verifications, nil
}
// IsVerifiedDomain checks if a domain is verified and not expired
func (k Keeper) IsVerifiedDomain(ctx context.Context, domain string) bool {
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err != nil {
return false
}
return verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED &&
!k.isDomainVerificationExpired(verification)
}
// generateVerificationToken creates a cryptographically secure random token
func (k Keeper) generateVerificationToken() (string, error) {
bytes := make([]byte, TokenLength)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
return hex.EncodeToString(bytes), nil
}
// checkDNSTXTRecord performs DNS TXT record lookup and validation
func (k Keeper) checkDNSTXTRecord(domain, expectedToken string) (bool, error) {
// Expected TXT record format: "sonr-verification=<token>"
expectedRecord := VerificationPrefix + expectedToken
// Perform DNS TXT lookup
txtRecords, err := net.LookupTXT(domain)
if err != nil {
return false, fmt.Errorf("DNS lookup failed: %w", err)
}
// Check if any TXT record matches our expected verification record
for _, record := range txtRecords {
if strings.TrimSpace(record) == expectedRecord {
return true, nil
}
}
return false, nil
}
// validateDomainFormat validates that a domain name is properly formatted
func (k Keeper) validateDomainFormat(domain string) error {
if domain == "" {
return fmt.Errorf("domain cannot be empty")
}
// Basic domain validation - check for valid characters and format
if len(domain) > 253 {
return fmt.Errorf("domain name too long")
}
// Check for valid domain format (basic validation)
if !strings.Contains(domain, ".") {
return fmt.Errorf("domain must contain at least one dot")
}
// Check for invalid characters
for _, char := range domain {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '.' || char == '-') {
return fmt.Errorf("domain contains invalid character: %c", char)
}
}
// Domain cannot start or end with a hyphen
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") {
return fmt.Errorf("domain cannot start or end with hyphen")
}
return nil
}
// isDomainVerificationValid checks if a domain verification is still valid (not expired)
func (k Keeper) isDomainVerificationValid(verification *v1.DomainVerification) bool {
if verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED {
return true // Verified domains don't expire
}
return !k.isDomainVerificationExpired(verification)
}
// isDomainVerificationExpired checks if a domain verification has expired
func (k Keeper) isDomainVerificationExpired(verification *v1.DomainVerification) bool {
now := time.Now().Unix()
return now > verification.ExpiresAt
}
// GetDNSInstructions returns human-readable instructions for setting up DNS verification
func (k Keeper) GetDNSInstructions(domain, token string) string {
return fmt.Sprintf(
"Add the following TXT record to your DNS configuration for domain '%s':\n\n"+
"Name: %s\n"+
"Type: TXT\n"+
"Value: %s%s\n\n"+
"Note: DNS propagation may take up to 48 hours. You can verify the record using:\n"+
"dig TXT %s",
domain, domain, VerificationPrefix, token, domain,
)
}
// SetDomainVerified is a helper method for testing to mark a domain as verified
func (k Keeper) SetDomainVerified(ctx context.Context, domain string) error {
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err != nil {
return fmt.Errorf("domain verification not found: %w", err)
}
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
return k.OrmDB.DomainVerificationTable().Update(ctx, verification)
}
+248
View File
@@ -0,0 +1,248 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
svcv1 "github.com/sonr-io/sonr/api/svc/v1"
"github.com/sonr-io/sonr/x/svc/types"
)
type EventsTestSuite struct {
suite.Suite
f *testFixture
}
func TestEventsTestSuite(t *testing.T) {
suite.Run(t, new(EventsTestSuite))
}
func (suite *EventsTestSuite) SetupTest() {
suite.f = SetupTest(suite.T())
}
// TestInitiateDomainVerificationEventEmission tests EventDomainVerificationInitiated
func (suite *EventsTestSuite) TestInitiateDomainVerificationEventEmission() {
domain := "example.com"
creator := suite.f.addrs[0].String()
msg := &types.MsgInitiateDomainVerification{
Domain: domain,
Creator: creator,
}
// Execute InitiateDomainVerification
resp, err := suite.f.msgServer.InitiateDomainVerification(suite.f.ctx, msg)
suite.Require().NoError(err)
suite.Require().NotNil(resp)
suite.Require().NotEmpty(resp.VerificationToken)
// Check for emitted events
events := suite.f.ctx.EventManager().Events()
suite.Require().NotEmpty(events, "Expected events to be emitted")
// Find the typed event - simplified check
var foundEvent bool
for _, event := range events {
if event.Type == "svc.v1.EventDomainVerificationInitiated" {
foundEvent = true
break
}
}
suite.Require().True(foundEvent, "EventDomainVerificationInitiated not found")
}
// TestVerifyDomainEventEmission tests EventDomainVerified emission
func (suite *EventsTestSuite) TestVerifyDomainEventEmission() {
domain := "verified.com"
creator := suite.f.addrs[0].String()
// First initiate verification
initMsg := &types.MsgInitiateDomainVerification{
Domain: domain,
Creator: creator,
}
initResp, err := suite.f.msgServer.InitiateDomainVerification(suite.f.ctx, initMsg)
suite.Require().NoError(err)
suite.Require().NotNil(initResp)
// Mock successful DNS verification by updating the verification status directly
// In real scenario, DNS would be checked
verification, err := suite.f.k.OrmDB.DomainVerificationTable().Get(suite.f.ctx, domain)
suite.Require().NoError(err)
suite.Require().NotNil(verification)
// Update status to verified (simulating successful DNS check)
verification.Status = svcv1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
err = suite.f.k.OrmDB.DomainVerificationTable().Update(suite.f.ctx, verification)
suite.Require().NoError(err)
// Clear events
suite.f.ctx = suite.f.ctx.WithEventManager(sdk.NewEventManager())
// Now verify the domain
verifyMsg := &types.MsgVerifyDomain{
Domain: domain,
Creator: creator,
}
verifyResp, err := suite.f.msgServer.VerifyDomain(suite.f.ctx, verifyMsg)
suite.Require().NoError(err)
suite.Require().NotNil(verifyResp)
suite.Require().True(verifyResp.Verified)
// Check for emitted events
events := suite.f.ctx.EventManager().Events()
suite.Require().NotEmpty(events, "Expected events to be emitted")
// Find the EventDomainVerified - simplified check
var foundEvent bool
for _, event := range events {
if event.Type == "svc.v1.EventDomainVerified" {
foundEvent = true
break
}
}
suite.Require().True(foundEvent, "EventDomainVerified not found")
}
// TestRegisterServiceEventEmission tests EventServiceRegistered emission
func (suite *EventsTestSuite) TestRegisterServiceEventEmission() {
domain := "service.com"
creator := suite.f.addrs[0].String()
serviceId := "test-service-001"
// Setup: Create and verify domain first
suite.setupVerifiedDomain(domain, creator)
// Clear events
suite.f.ctx = suite.f.ctx.WithEventManager(sdk.NewEventManager())
// Register service
msg := &types.MsgRegisterService{
ServiceId: serviceId,
Domain: domain,
Creator: creator,
RequestedPermissions: []string{"read", "write"},
}
resp, err := suite.f.msgServer.RegisterService(suite.f.ctx, msg)
suite.Require().NoError(err)
suite.Require().NotNil(resp)
suite.Require().Equal(serviceId, resp.ServiceId)
// Check for emitted events
events := suite.f.ctx.EventManager().Events()
suite.Require().NotEmpty(events, "Expected events to be emitted")
// Find the EventServiceRegistered - simplified check
var foundEvent bool
for _, event := range events {
if event.Type == "svc.v1.EventServiceRegistered" {
foundEvent = true
break
}
}
suite.Require().True(foundEvent, "EventServiceRegistered not found")
}
// TestFailedVerificationNoEventEmission tests no event on failed verification
func (suite *EventsTestSuite) TestFailedVerificationNoEventEmission() {
domain := "unverified.com"
creator := suite.f.addrs[0].String()
// Initiate verification but don't set DNS record
initMsg := &types.MsgInitiateDomainVerification{
Domain: domain,
Creator: creator,
}
_, err := suite.f.msgServer.InitiateDomainVerification(suite.f.ctx, initMsg)
suite.Require().NoError(err)
// Clear events
suite.f.ctx = suite.f.ctx.WithEventManager(sdk.NewEventManager())
// Try to verify without DNS record (should fail)
verifyMsg := &types.MsgVerifyDomain{
Domain: domain,
Creator: creator,
}
resp, err := suite.f.msgServer.VerifyDomain(suite.f.ctx, verifyMsg)
suite.Require().NoError(err) // Returns success with verified=false
suite.Require().False(resp.Verified)
// Check that no EventDomainVerified was emitted
events := suite.f.ctx.EventManager().Events()
// Look for EventDomainVerified - should not find it
var foundVerifiedEvent bool
for _, event := range events {
if event.Type == "svc.v1.EventDomainVerified" {
foundVerifiedEvent = true
break
}
}
suite.Require().
False(foundVerifiedEvent, "EventDomainVerified should not be emitted on failure")
}
// TestErrorCaseNoEventEmission tests no events on error
func (suite *EventsTestSuite) TestErrorCaseNoEventEmission() {
// Try to register service without verified domain
msg := &types.MsgRegisterService{
ServiceId: "invalid-service",
Domain: "notverified.com",
Creator: suite.f.addrs[0].String(),
RequestedPermissions: []string{"read"},
}
// Clear any previous events
suite.f.ctx = suite.f.ctx.WithEventManager(sdk.NewEventManager())
// Execute RegisterService - should fail
_, err := suite.f.msgServer.RegisterService(suite.f.ctx, msg)
suite.Require().Error(err)
// Check that no events were emitted (except potentially message events)
events := suite.f.ctx.EventManager().Events()
// Filter out message events
var nonMessageEvents []sdk.Event
for _, event := range events {
if event.Type != sdk.EventTypeMessage {
nonMessageEvents = append(nonMessageEvents, event)
}
}
suite.Require().Empty(nonMessageEvents, "Expected no events to be emitted on error")
}
// Helper function to setup a verified domain
func (suite *EventsTestSuite) setupVerifiedDomain(domain, creator string) {
// Initiate verification
initMsg := &types.MsgInitiateDomainVerification{
Domain: domain,
Creator: creator,
}
_, err := suite.f.msgServer.InitiateDomainVerification(suite.f.ctx, initMsg)
suite.Require().NoError(err)
// Mock successful verification
verification, err := suite.f.k.OrmDB.DomainVerificationTable().Get(suite.f.ctx, domain)
suite.Require().NoError(err)
suite.Require().NotNil(verification)
verification.Status = svcv1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
err = suite.f.k.OrmDB.DomainVerificationTable().Update(suite.f.ctx, verification)
suite.Require().NoError(err)
}
Regular → Executable
+1 -2
View File
@@ -3,7 +3,7 @@ package keeper_test
import (
"testing"
"github.com/sonr-io/snrd/x/svc/types"
"github.com/sonr-io/sonr/x/svc/types"
"github.com/stretchr/testify/require"
)
@@ -18,5 +18,4 @@ func TestGenesis(t *testing.T) {
got := f.k.ExportGenesis(f.ctx)
require.NotNil(t, got)
}
+362 -9
View File
@@ -2,6 +2,8 @@ package keeper
import (
"context"
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/codec"
@@ -13,8 +15,10 @@ import (
"cosmossdk.io/log"
"cosmossdk.io/orm/model/ormdb"
apiv1 "github.com/sonr-io/snrd/api/svc/v1"
"github.com/sonr-io/snrd/x/svc/types"
apiv1 "github.com/sonr-io/sonr/api/svc/v1"
"github.com/sonr-io/sonr/crypto/keys"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/sonr/x/svc/types"
)
type Keeper struct {
@@ -27,6 +31,13 @@ type Keeper struct {
Params collections.Item[types.Params]
OrmDB apiv1.StateStore
// dependencies
didKeeper types.DIDKeeper
// UCAN functionality
ucanVerifier *ucan.Verifier
permissionValidator *PermissionValidator
authority string
}
@@ -36,6 +47,7 @@ func NewKeeper(
storeService storetypes.KVStoreService,
logger log.Logger,
authority string,
didKeeper types.DIDKeeper,
) Keeper {
logger = logger.With(log.ModuleKey, "x/"+types.ModuleName)
@@ -45,7 +57,10 @@ func NewKeeper(
authority = authtypes.NewModuleAddress(govtypes.ModuleName).String()
}
db, err := ormdb.NewModuleDB(&types.ORMModuleSchema, ormdb.ModuleDBOptions{KVStoreService: storeService})
db, err := ormdb.NewModuleDB(
&types.ORMModuleSchema,
ormdb.ModuleDBOptions{KVStoreService: storeService},
)
if err != nil {
panic(err)
}
@@ -55,14 +70,25 @@ func NewKeeper(
panic(err)
}
// Create UCAN verifier with DID resolver
didResolver := &DIDKeeperResolver{didKeeper: didKeeper}
ucanVerifier := ucan.NewVerifier(didResolver)
k := Keeper{
cdc: cdc,
logger: logger,
Params: collections.NewItem(sb, types.ParamsKey, "params", codec.CollValue[types.Params](cdc)),
OrmDB: store,
Params: collections.NewItem(
sb,
types.ParamsKey,
"params",
codec.CollValue[types.Params](cdc),
),
OrmDB: store,
authority: authority,
didKeeper: didKeeper,
ucanVerifier: ucanVerifier,
authority: authority,
}
schema, err := sb.Build()
@@ -72,21 +98,51 @@ func NewKeeper(
k.Schema = schema
// Initialize UCAN permission validator (after keeper is fully constructed)
k.permissionValidator = NewPermissionValidator(k)
return k
}
// GetPermissionValidator returns the UCAN permission validator
func (k Keeper) GetPermissionValidator() *PermissionValidator {
return k.permissionValidator
}
func (k Keeper) Logger() log.Logger {
return k.logger
}
// InitGenesis initializes the module's state from a genesis state.
func (k *Keeper) InitGenesis(ctx context.Context, data *types.GenesisState) error {
if err := data.Params.Validate(); err != nil {
return err
}
return k.Params.Set(ctx, data.Params)
// Set parameters
if err := k.Params.Set(ctx, data.Params); err != nil {
return err
}
// Import capabilities
for _, capability := range data.Capabilities {
// Convert to types.ServiceCapability for storage
cap := &types.ServiceCapability{
CapabilityId: capability.CapabilityId,
ServiceId: capability.ServiceId,
Domain: capability.Domain,
Abilities: capability.Abilities,
Owner: capability.Owner,
CreatedAt: capability.CreatedAt,
ExpiresAt: capability.ExpiresAt,
Revoked: capability.Revoked,
}
if err := k.StoreCapability(ctx, cap); err != nil {
return fmt.Errorf("failed to import capability %s: %w", capability.CapabilityId, err)
}
}
return nil
}
// ExportGenesis exports the module's state to a genesis state.
@@ -96,7 +152,304 @@ func (k *Keeper) ExportGenesis(ctx context.Context) *types.GenesisState {
panic(err)
}
// Export all capabilities
var capabilities []types.ServiceCapability
// Iterate through all capabilities in the ORM
iter, err := k.OrmDB.ServiceCapabilityTable().List(ctx, apiv1.ServiceCapabilityPrimaryKey{})
if err != nil {
panic(fmt.Errorf("failed to list capabilities for export: %w", err))
}
defer iter.Close()
for iter.Next() {
apiCap, err := iter.Value()
if err != nil {
panic(fmt.Errorf("failed to get capability during export: %w", err))
}
// Convert from API type to types
cap := types.ServiceCapability{
CapabilityId: apiCap.CapabilityId,
ServiceId: apiCap.ServiceId,
Domain: apiCap.Domain,
Abilities: apiCap.Abilities,
Owner: apiCap.Owner,
CreatedAt: apiCap.CreatedAt,
ExpiresAt: apiCap.ExpiresAt,
Revoked: apiCap.Revoked,
}
capabilities = append(capabilities, cap)
}
return &types.GenesisState{
Params: params,
Params: params,
Capabilities: capabilities,
}
}
// VerifyServiceRegistration verifies service registration and domain ownership
func (k Keeper) VerifyServiceRegistration(
ctx context.Context,
serviceID string,
domain string,
) (bool, error) {
if serviceID == "" {
return false, types.ErrInvalidServiceID
}
if domain == "" {
return false, types.ErrDomainNotVerified
}
// Check if the service exists
service, err := k.OrmDB.ServiceTable().Get(ctx, serviceID)
if err != nil {
return false, types.ErrInvalidServiceID
}
// Verify the service belongs to the specified domain
if service.Domain != domain {
return false, types.ErrDomainNotVerified
}
// Check if the domain is verified
if !k.IsVerifiedDomain(ctx, domain) {
return false, types.ErrDomainNotVerified
}
// Check if the service is active
if service.Status != apiv1.ServiceStatus_SERVICE_STATUS_ACTIVE {
return false, types.ErrInvalidServiceID
}
return true, nil
}
// GetService gets service by ID
func (k Keeper) GetService(ctx context.Context, serviceID string) (*types.Service, error) {
if serviceID == "" {
return nil, types.ErrInvalidServiceID
}
// Get service from ORM
service, err := k.OrmDB.ServiceTable().Get(ctx, serviceID)
if err != nil {
return nil, types.ErrInvalidServiceID
}
// Convert v1.Service to types.Service
return &types.Service{
Id: service.Id,
Domain: service.Domain,
Owner: service.Owner,
RootCapabilityCid: service.RootCapabilityCid,
Permissions: service.Permissions,
Status: types.ServiceStatus(service.Status),
CreatedAt: service.CreatedAt,
UpdatedAt: service.UpdatedAt,
}, nil
}
// IsDomainVerified checks if domain is verified
func (k Keeper) IsDomainVerified(ctx context.Context, domain string, owner string) (bool, error) {
if domain == "" {
return false, types.ErrDomainNotVerified
}
// Get domain verification record
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err != nil {
return false, types.ErrDomainNotVerified
}
// Check if the domain is verified
if verification.Status != apiv1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED {
return false, nil
}
// Check if the owner matches (if provided)
if owner != "" && verification.Owner != owner {
return false, nil
}
// Check if the verification hasn't expired
if k.isDomainVerificationExpired(verification) {
return false, nil
}
return true, nil
}
// GetServicesByDomain gets services by domain
func (k Keeper) GetServicesByDomain(ctx context.Context, domain string) ([]types.Service, error) {
if domain == "" {
return nil, types.ErrDomainNotVerified
}
// Create index key for domain
domainKey := apiv1.ServiceDomainIndexKey{}.WithDomain(domain)
// List services by domain
iter, err := k.OrmDB.ServiceTable().List(ctx, domainKey)
if err != nil {
return nil, err
}
defer iter.Close()
var services []types.Service
for iter.Next() {
service, err := iter.Value()
if err != nil {
return nil, err
}
// Convert v1.Service to types.Service
services = append(services, types.Service{
Id: service.Id,
Domain: service.Domain,
Owner: service.Owner,
RootCapabilityCid: service.RootCapabilityCid,
Permissions: service.Permissions,
Status: types.ServiceStatus(service.Status),
CreatedAt: service.CreatedAt,
UpdatedAt: service.UpdatedAt,
})
}
return services, nil
}
// VerifyOrigin validates a relying party origin for WebAuthn operations
func (k Keeper) VerifyOrigin(ctx context.Context, origin string) error {
// Allow localhost origins for development
if isLocalhostOrigin(origin) {
return nil
}
// Extract domain from origin
domain := extractDomainFromOrigin(origin)
if domain == "" {
return fmt.Errorf("could not extract domain from origin: %s", origin)
}
// Check if domain is verified
if !k.IsVerifiedDomain(ctx, domain) {
return fmt.Errorf("domain not verified: %s", domain)
}
// Check if there are active services for this domain
services, err := k.GetServicesByDomain(ctx, domain)
if err != nil {
return fmt.Errorf("failed to get services for domain %s: %w", domain, err)
}
if len(services) == 0 {
return fmt.Errorf("no services registered for domain: %s", domain)
}
// Check if at least one service is active
hasActiveService := false
for _, service := range services {
if service.Status == types.ServiceStatus_SERVICE_STATUS_ACTIVE {
hasActiveService = true
break
}
}
if !hasActiveService {
return fmt.Errorf("no active services found for domain: %s", domain)
}
return nil
}
// isLocalhostOrigin checks if the origin is a localhost origin
func isLocalhostOrigin(origin string) bool {
localhostPatterns := []string{
"http://localhost",
"https://localhost",
"http://127.0.0.1",
"https://127.0.0.1",
"http://[::1]",
"https://[::1]",
}
for _, pattern := range localhostPatterns {
if strings.HasPrefix(origin, pattern) {
return true
}
}
return false
}
// extractDomainFromOrigin extracts the domain from an origin URL
func extractDomainFromOrigin(origin string) string {
// Remove protocol
domain := strings.TrimPrefix(origin, "https://")
domain = strings.TrimPrefix(domain, "http://")
// Remove port if present
if idx := strings.Index(domain, ":"); idx != -1 {
domain = domain[:idx]
}
// Remove path if present
if idx := strings.Index(domain, "/"); idx != -1 {
domain = domain[:idx]
}
return domain
}
// ValidateServiceOwnerDID verifies that the service owner has a valid DID document
func (k Keeper) ValidateServiceOwnerDID(ctx context.Context, ownerDID string) error {
if ownerDID == "" {
return types.ErrInvalidOwnerDID
}
// Get the DID document
didDoc, err := k.didKeeper.GetDIDDocument(ctx, ownerDID)
if err != nil {
return types.ErrInvalidOwnerDID
}
// Check if the DID document exists
if didDoc == nil {
return types.ErrInvalidOwnerDID
}
// Check if the DID document is deactivated
if didDoc.Deactivated {
return types.ErrInvalidOwnerDID
}
// Additional validation: check if the DID document has valid verification methods
if len(didDoc.VerificationMethod) == 0 {
return types.ErrInvalidOwnerDID
}
return nil
}
// DIDKeeperResolver adapts the DID keeper to implement the UCAN DIDResolver interface
type DIDKeeperResolver struct {
didKeeper types.DIDKeeper
}
// ResolveDIDKey resolves a DID string using the DID keeper
func (r *DIDKeeperResolver) ResolveDIDKey(ctx context.Context, did string) (keys.DID, error) {
// Get the DID document from the keeper
didDoc, err := r.didKeeper.GetDIDDocument(ctx, did)
if err != nil {
return keys.DID{}, err
}
if didDoc == nil {
return keys.DID{}, types.ErrInvalidOwnerDID
}
// Parse the DID string into a keys.DID
// This assumes the DID keeper can provide the public key information
return keys.Parse(did)
}
+266 -12
View File
@@ -1,20 +1,22 @@
package keeper_test
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
"cosmossdk.io/core/address"
"cosmossdk.io/log"
storetypes "cosmossdk.io/store/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
sdkaddress "github.com/cosmos/cosmos-sdk/codec/address"
"github.com/cosmos/cosmos-sdk/runtime"
"github.com/cosmos/cosmos-sdk/testutil/integration"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
authcodec "github.com/cosmos/cosmos-sdk/x/auth/codec"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
@@ -25,9 +27,11 @@ import (
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
module "github.com/sonr-io/snrd/x/svc"
"github.com/sonr-io/snrd/x/svc/keeper"
"github.com/sonr-io/snrd/x/svc/types"
"github.com/sonr-io/sonr/app"
didtypes "github.com/sonr-io/sonr/x/did/types"
module "github.com/sonr-io/sonr/x/svc"
"github.com/sonr-io/sonr/x/svc/keeper"
"github.com/sonr-io/sonr/x/svc/types"
)
var maccPerms = map[string][]string{
@@ -38,6 +42,62 @@ var maccPerms = map[string][]string{
govtypes.ModuleName: {authtypes.Burner},
}
// SVCMockDIDKeeper provides a minimal mock implementation for SVC testing
type SVCMockDIDKeeper struct{}
func (m *SVCMockDIDKeeper) ResolveDID(
ctx context.Context,
did string,
) (*didtypes.DIDDocument, *didtypes.DIDDocumentMetadata, error) {
if did == "did:example:deactivated" {
return &didtypes.DIDDocument{
Id: did,
Deactivated: true,
}, &didtypes.DIDDocumentMetadata{
Did: did,
}, nil
}
if did == "did:example:no-verification" {
return &didtypes.DIDDocument{
Id: did,
VerificationMethod: []*didtypes.VerificationMethod{},
}, &didtypes.DIDDocumentMetadata{
Did: did,
}, nil
}
return &didtypes.DIDDocument{
Id: did,
VerificationMethod: []*didtypes.VerificationMethod{{Id: did + "#key-1"}},
}, &didtypes.DIDDocumentMetadata{Did: did}, nil
}
func (m *SVCMockDIDKeeper) GetDIDDocument(
ctx context.Context,
did string,
) (*didtypes.DIDDocument, error) {
if did == "did:example:deactivated" {
return &didtypes.DIDDocument{Id: did, Deactivated: true}, nil
}
if did == "did:example:no-verification" {
return &didtypes.DIDDocument{
Id: did,
VerificationMethod: []*didtypes.VerificationMethod{},
}, nil
}
return &didtypes.DIDDocument{
Id: did,
VerificationMethod: []*didtypes.VerificationMethod{{Id: did + "#key-1"}},
}, nil
}
func (m *SVCMockDIDKeeper) VerifyDIDDocumentSignature(
ctx context.Context,
did string,
signature []byte,
) (bool, error) {
return true, nil
}
type testFixture struct {
suite.Suite
@@ -60,6 +120,16 @@ func SetupTest(t *testing.T) *testFixture {
t.Helper()
f := new(testFixture)
cfg := sdk.GetConfig() // do not seal, more set later
cfg.SetBech32PrefixForAccount(app.Bech32PrefixAccAddr, app.Bech32PrefixAccPub)
cfg.SetBech32PrefixForValidator(app.Bech32PrefixValAddr, app.Bech32PrefixValPub)
cfg.SetBech32PrefixForConsensusNode(app.Bech32PrefixConsAddr, app.Bech32PrefixConsPub)
cfg.SetCoinType(app.CoinType)
validatorAddressCodec := sdkaddress.NewBech32Codec(app.Bech32PrefixValAddr)
accountAddressCodec := sdkaddress.NewBech32Codec(app.Bech32PrefixAccAddr)
consensusAddressCodec := sdkaddress.NewBech32Codec(app.Bech32PrefixConsAddr)
// Base setup
logger := log.NewTestLogger(t)
encCfg := moduletestutil.MakeTestEncodingConfig()
@@ -67,14 +137,40 @@ func SetupTest(t *testing.T) *testFixture {
f.govModAddr = authtypes.NewModuleAddress(govtypes.ModuleName).String()
f.addrs = simtestutil.CreateIncrementalAccounts(3)
keys := storetypes.NewKVStoreKeys(authtypes.ModuleName, banktypes.ModuleName, stakingtypes.ModuleName, minttypes.ModuleName, types.ModuleName)
f.ctx = sdk.NewContext(integration.CreateMultiStore(keys, logger), cmtproto.Header{}, false, logger)
keys := storetypes.NewKVStoreKeys(
authtypes.ModuleName,
banktypes.ModuleName,
stakingtypes.ModuleName,
minttypes.ModuleName,
types.ModuleName,
)
f.ctx = sdk.NewContext(
integration.CreateMultiStore(keys, logger),
cmtproto.Header{},
false,
logger,
)
// Register SDK modules.
registerBaseSDKModules(logger, f, encCfg, keys)
registerBaseSDKModules(
logger,
f,
encCfg,
keys,
accountAddressCodec,
validatorAddressCodec,
consensusAddressCodec,
)
// Setup Keeper.
f.k = keeper.NewKeeper(encCfg.Codec, runtime.NewKVStoreService(keys[types.ModuleName]), logger, f.govModAddr)
// Setup SVC Keeper with DID dependency only (UCAN is now internal).
mockDIDKeeper := &SVCMockDIDKeeper{}
f.k = keeper.NewKeeper(
encCfg.Codec,
runtime.NewKVStoreService(keys[types.ModuleName]),
logger,
f.govModAddr,
mockDIDKeeper,
)
f.msgServer = keeper.NewMsgServerImpl(f.k)
f.queryServer = keeper.NewQuerier(f.k)
f.appModule = module.NewAppModule(encCfg.Codec, f.k)
@@ -96,6 +192,9 @@ func registerBaseSDKModules(
f *testFixture,
encCfg moduletestutil.TestEncodingConfig,
keys map[string]*storetypes.KVStoreKey,
ac address.Codec,
validator address.Codec,
consensus address.Codec,
) {
registerModuleInterfaces(encCfg)
@@ -104,7 +203,7 @@ func registerBaseSDKModules(
encCfg.Codec, runtime.NewKVStoreService(keys[authtypes.StoreKey]),
authtypes.ProtoBaseAccount,
maccPerms,
authcodec.NewBech32Codec(sdk.Bech32MainPrefix), sdk.Bech32MainPrefix,
ac, app.Bech32PrefixAccAddr,
f.govModAddr,
)
@@ -120,8 +219,8 @@ func registerBaseSDKModules(
f.stakingKeeper = stakingkeeper.NewKeeper(
encCfg.Codec, runtime.NewKVStoreService(keys[stakingtypes.StoreKey]),
f.accountkeeper, f.bankkeeper, f.govModAddr,
authcodec.NewBech32Codec(sdk.Bech32PrefixValAddr),
authcodec.NewBech32Codec(sdk.Bech32PrefixConsAddr),
validator,
consensus,
)
// Mint Keeper.
@@ -131,3 +230,158 @@ func registerBaseSDKModules(
authtypes.FeeCollectorName, f.govModAddr,
)
}
// Test for VerifyServiceRegistration method
func TestVerifyServiceRegistration(t *testing.T) {
f := SetupTest(t)
// Test case 1: Invalid service ID
valid, err := f.k.VerifyServiceRegistration(f.ctx, "", "example.com")
if err != types.ErrInvalidServiceID {
t.Errorf("Expected ErrInvalidServiceID, got %v", err)
}
if valid {
t.Error("Expected invalid service registration")
}
// Test case 2: Invalid domain
valid, err = f.k.VerifyServiceRegistration(f.ctx, "test-service", "")
if err != types.ErrDomainNotVerified {
t.Errorf("Expected ErrDomainNotVerified, got %v", err)
}
if valid {
t.Error("Expected invalid service registration")
}
// Test case 3: Non-existent service
valid, err = f.k.VerifyServiceRegistration(f.ctx, "non-existent-service", "example.com")
if err != types.ErrInvalidServiceID {
t.Errorf("Expected ErrInvalidServiceID, got %v", err)
}
if valid {
t.Error("Expected invalid service registration")
}
}
// Test for GetService method
func TestGetService(t *testing.T) {
f := SetupTest(t)
// Test case 1: Invalid service ID
service, err := f.k.GetService(f.ctx, "")
if err != types.ErrInvalidServiceID {
t.Errorf("Expected ErrInvalidServiceID, got %v", err)
}
if service != nil {
t.Error("Expected nil service")
}
// Test case 2: Non-existent service
service, err = f.k.GetService(f.ctx, "non-existent-service")
if err != types.ErrInvalidServiceID {
t.Errorf("Expected ErrInvalidServiceID, got %v", err)
}
if service != nil {
t.Error("Expected nil service")
}
}
// Test for IsDomainVerified method
func TestIsDomainVerified(t *testing.T) {
f := SetupTest(t)
// Test case 1: Empty domain
verified, err := f.k.IsDomainVerified(f.ctx, "", "owner")
if err != types.ErrDomainNotVerified {
t.Errorf("Expected ErrDomainNotVerified, got %v", err)
}
if verified {
t.Error("Expected domain not verified")
}
// Test case 2: Non-existent domain
verified, err = f.k.IsDomainVerified(f.ctx, "non-existent.com", "owner")
if err != types.ErrDomainNotVerified {
t.Errorf("Expected ErrDomainNotVerified, got %v", err)
}
if verified {
t.Error("Expected domain not verified")
}
}
// Test for GetServicesByDomain method
func TestGetServicesByDomain(t *testing.T) {
f := SetupTest(t)
// Test case 1: Empty domain
services, err := f.k.GetServicesByDomain(f.ctx, "")
if err != types.ErrDomainNotVerified {
t.Errorf("Expected ErrDomainNotVerified, got %v", err)
}
if services != nil {
t.Error("Expected nil services")
}
// Test case 2: Non-existent domain
services, err = f.k.GetServicesByDomain(f.ctx, "non-existent.com")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if len(services) != 0 {
t.Error("Expected empty services slice")
}
}
// Test for ValidateServiceOwnerDID method
func TestValidateServiceOwnerDID(t *testing.T) {
f := SetupTest(t)
// Test case 1: Empty DID
err := f.k.ValidateServiceOwnerDID(f.ctx, "")
if err != types.ErrInvalidOwnerDID {
t.Errorf("Expected ErrInvalidOwnerDID, got %v", err)
}
// Test case 2: Valid DID (mocked)
err = f.k.ValidateServiceOwnerDID(f.ctx, "did:example:123")
if err != nil {
t.Errorf("Expected no error for valid DID, got %v", err)
}
// Test case 3: Deactivated DID
err = f.k.ValidateServiceOwnerDID(f.ctx, "did:example:deactivated")
if err != types.ErrInvalidOwnerDID {
t.Errorf("Expected ErrInvalidOwnerDID for deactivated DID, got %v", err)
}
// Test case 4: DID without verification methods
err = f.k.ValidateServiceOwnerDID(f.ctx, "did:example:no-verification")
if err != types.ErrInvalidOwnerDID {
t.Errorf("Expected ErrInvalidOwnerDID for DID without verification methods, got %v", err)
}
}
// Test for internal UCAN integration basic functionality
func TestInternalUCANIntegrationBasic(t *testing.T) {
f := SetupTest(t)
// Test that keeper was created successfully with internal UCAN integration
if f.k.Logger() == nil {
t.Error("Expected keeper to be properly initialized")
}
// Test ValidateServicePermissions method (which uses internal validation)
permissions := []string{"register", "update"}
err := f.k.ValidateServicePermissions(f.ctx, permissions)
if err != nil {
t.Errorf("ValidateServicePermissions failed: %v", err)
}
// Test UCAN delegation chain validation (which uses internal UCAN library)
// Note: This will fail with properly formatted error since we don't have valid UCAN tokens
invalidChain := "invalid_token"
err = f.k.ValidateUCANDelegationChain(f.ctx, invalidChain)
if err == nil {
t.Error("Expected ValidateUCANDelegationChain to fail with invalid token")
}
}
Regular → Executable
+328 -8
View File
@@ -2,11 +2,15 @@ package keeper
import (
"context"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"cosmossdk.io/errors"
"github.com/sonr-io/snrd/x/svc/types"
v1 "github.com/sonr-io/sonr/api/svc/v1"
"github.com/sonr-io/sonr/x/svc/types"
)
type msgServer struct {
@@ -20,17 +24,333 @@ func NewMsgServerImpl(keeper Keeper) types.MsgServer {
return &msgServer{k: keeper}
}
func (ms msgServer) UpdateParams(ctx context.Context, msg *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) {
// UCAN validation helper functions
// extractUCANToken extracts UCAN token from transaction context
func (ms msgServer) extractUCANToken(ctx context.Context) (string, bool) {
// In production, UCAN tokens would be extracted from:
// 1. Transaction metadata set by ante handlers
// 2. Message extension fields
// 3. Transaction memo field
// For now, we return false to proceed with normal validation
return "", false
}
// validateUCANPermission validates UCAN authorization for a Service operation
func (ms msgServer) validateUCANPermission(
ctx context.Context,
serviceID string,
operation types.ServiceOperation,
) error {
// Try to extract UCAN token
tokenString, hasToken := ms.extractUCANToken(ctx)
if !hasToken {
// No UCAN token present, proceed with normal validation
return nil
}
// Validate UCAN token for the specific operation
validator := ms.k.GetPermissionValidator()
if validator == nil {
return fmt.Errorf("UCAN permission validator not initialized")
}
// Use general permission validation
return validator.ValidatePermission(ctx, tokenString, serviceID, operation)
}
// validateDomainBoundUCANPermission validates UCAN authorization for domain-bound operations
func (ms msgServer) validateDomainBoundUCANPermission(
ctx context.Context,
domain string,
serviceID string,
operation types.ServiceOperation,
) error {
// Try to extract UCAN token
tokenString, hasToken := ms.extractUCANToken(ctx)
if !hasToken {
// No UCAN token present, proceed with normal validation
return nil
}
// Validate domain-bound UCAN token
validator := ms.k.GetPermissionValidator()
if validator == nil {
return fmt.Errorf("UCAN permission validator not initialized")
}
return validator.ValidateDomainBoundPermission(ctx, tokenString, domain, serviceID, operation)
}
// checkGaslessSupport checks if the operation can be executed gaslessly via UCAN
func (ms msgServer) checkGaslessSupport(
ctx context.Context,
serviceID string,
operation types.ServiceOperation,
) (bool, uint64) {
// Try to extract UCAN token
tokenString, hasToken := ms.extractUCANToken(ctx)
if !hasToken {
return false, 0
}
// Check gasless support
validator := ms.k.GetPermissionValidator()
if validator == nil {
return false, 0
}
supportsGasless, gasLimit, err := validator.SupportsGaslessTransaction(ctx, tokenString, serviceID, operation)
if err != nil {
ms.k.Logger().Debug("Failed to check gasless support", "error", err)
return false, 0
}
return supportsGasless, gasLimit
}
func (ms msgServer) UpdateParams(
ctx context.Context,
msg *types.MsgUpdateParams,
) (*types.MsgUpdateParamsResponse, error) {
if ms.k.authority != msg.Authority {
return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", ms.k.authority, msg.Authority)
return nil, errors.Wrapf(
govtypes.ErrInvalidSigner,
"invalid authority; expected %s, got %s",
ms.k.authority,
msg.Authority,
)
}
return nil, ms.k.Params.Set(ctx, msg.Params)
}
// RegisterService implements types.MsgServer.
func (ms msgServer) RegisterService(ctx context.Context, msg *types.MsgRegisterService) (*types.MsgRegisterServiceResponse, error) {
// ctx := sdk.UnwrapSDKContext(goCtx)
panic("RegisterService is unimplemented")
return &types.MsgRegisterServiceResponse{}, nil
// InitiateDomainVerification implements types.MsgServer.
func (ms msgServer) InitiateDomainVerification(
ctx context.Context,
msg *types.MsgInitiateDomainVerification,
) (*types.MsgInitiateDomainVerificationResponse, error) {
// UCAN authorization validation for domain verification
if err := ms.validateDomainBoundUCANPermission(ctx, msg.Domain, "", types.ServiceOpInitiateDomainVerification); err != nil {
return nil, errors.Wrapf(types.ErrInvalidUCANDelegation, "UCAN validation failed: %v", err)
}
// Check for gasless execution support
supportsGasless, gasLimit := ms.checkGaslessSupport(ctx, msg.Domain, types.ServiceOpInitiateDomainVerification)
if supportsGasless {
// Log gasless execution (in production, this might set transaction fees to zero)
ms.k.Logger().Info("Executing domain verification with gasless transaction",
"domain", msg.Domain,
"gas_limit", gasLimit)
}
// Initiate domain verification using the keeper
verification, err := ms.k.InitiateDomainVerification(ctx, msg.Domain, msg.Creator)
if err != nil {
return nil, err
}
// Generate DNS instructions for the user
dnsInstructions := ms.k.GetDNSInstructions(msg.Domain, verification.VerificationToken)
// Emit typed event
sdkCtx := sdk.UnwrapSDKContext(ctx)
event := &types.EventDomainVerificationInitiated{
Domain: msg.Domain,
VerificationId: msg.Domain, // Using domain as ID since it's unique
Challenge: verification.VerificationToken,
Initiator: msg.Creator,
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventDomainVerificationInitiated")
}
return &types.MsgInitiateDomainVerificationResponse{
VerificationToken: verification.VerificationToken,
DnsInstruction: dnsInstructions,
}, nil
}
// VerifyDomain implements types.MsgServer.
func (ms msgServer) VerifyDomain(
ctx context.Context,
msg *types.MsgVerifyDomain,
) (*types.MsgVerifyDomainResponse, error) {
// UCAN authorization validation for domain verification
if err := ms.validateDomainBoundUCANPermission(ctx, msg.Domain, "", types.ServiceOpVerifyDomain); err != nil {
return nil, errors.Wrapf(types.ErrInvalidUCANDelegation, "UCAN validation failed: %v", err)
}
// Verify domain ownership by checking DNS TXT records
verification, err := ms.k.VerifyDomainOwnership(ctx, msg.Domain)
if err != nil {
return &types.MsgVerifyDomainResponse{
Verified: false,
Message: err.Error(),
}, nil // Return error message in response, not as gRPC error
}
// Check verification result
verified := verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
message := "Domain verification successful"
if !verified {
message = "Domain verification failed - DNS TXT record not found or incorrect"
} else {
// Emit typed event for successful verification
sdkCtx := sdk.UnwrapSDKContext(ctx)
event := &types.EventDomainVerified{
Domain: msg.Domain,
VerificationId: msg.Domain, // Using domain as ID
Verifier: msg.Creator,
VerifiedAt: sdkCtx.BlockTime(),
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventDomainVerified")
}
}
return &types.MsgVerifyDomainResponse{
Verified: verified,
Message: message,
}, nil
}
// RegisterService implements types.MsgServer.
func (ms msgServer) RegisterService(
ctx context.Context,
msg *types.MsgRegisterService,
) (*types.MsgRegisterServiceResponse, error) {
// UCAN authorization validation for service registration
if err := ms.validateDomainBoundUCANPermission(ctx, msg.Domain, msg.ServiceId, types.ServiceOpRegister); err != nil {
return nil, errors.Wrapf(types.ErrInvalidUCANDelegation, "UCAN validation failed: %v", err)
}
// Check for gasless execution support
supportsGasless, gasLimit := ms.checkGaslessSupport(ctx, msg.ServiceId, types.ServiceOpRegister)
if supportsGasless {
// Log gasless execution (in production, this might set transaction fees to zero)
ms.k.Logger().Info("Executing service registration with gasless transaction",
"service_id", msg.ServiceId,
"domain", msg.Domain,
"gas_limit", gasLimit)
}
// 1. Verify domain ownership
if !ms.k.IsVerifiedDomain(ctx, msg.Domain) {
return nil, errors.Wrapf(
types.ErrDomainNotVerified,
"domain %s is not verified",
msg.Domain,
)
}
// 2. Validate service owner DID
if err := ms.k.ValidateServiceOwnerDID(ctx, msg.Creator); err != nil {
return nil, errors.Wrapf(types.ErrInvalidOwnerDID, "owner DID validation failed: %v", err)
}
// 3. Validate service ID format
if msg.ServiceId == "" {
return nil, errors.Wrap(types.ErrInvalidServiceID, "service ID cannot be empty")
}
// 4. Check if service ID already exists
existing, err := ms.k.OrmDB.ServiceTable().Get(ctx, msg.ServiceId)
if err == nil && existing != nil {
return nil, errors.Wrapf(
types.ErrServiceAlreadyExists,
"service with ID %s already exists",
msg.ServiceId,
)
}
// 5. Check if domain is already bound to another service
existingByDomain, err := ms.k.OrmDB.ServiceTable().GetByDomain(ctx, msg.Domain)
if err == nil && existingByDomain != nil {
return nil, errors.Wrapf(
types.ErrDomainAlreadyBound,
"domain %s is already bound to service %s",
msg.Domain,
existingByDomain.Id,
)
}
// 6. Validate requested permissions
if err := ms.k.ValidateServicePermissions(ctx, msg.RequestedPermissions); err != nil {
return nil, errors.Wrap(types.ErrInvalidPermissions, err.Error())
}
// 7. Validate UCAN delegation chain if provided
if msg.UcanDelegationChain != "" {
err := ms.k.ValidateUCANDelegationChain(
ctx,
msg.UcanDelegationChain,
)
if err != nil {
return nil, errors.Wrap(types.ErrInvalidUCANDelegation, err.Error())
}
// Additionally validate the UCAN token grants the required permissions for the domain
resource := fmt.Sprintf("service://%s", msg.Domain)
_, err = ms.k.ValidateUCANToken(
ctx,
msg.UcanDelegationChain,
resource,
msg.RequestedPermissions,
)
if err != nil {
return nil, errors.Wrap(
types.ErrInvalidUCANDelegation,
fmt.Sprintf("UCAN token validation failed: %v", err),
)
}
}
// 8. Create root capability for the service
rootCapabilityCID, err := ms.k.CreateServiceRootCapability(ctx, msg)
if err != nil {
return nil, errors.Wrap(types.ErrFailedToCreateCapability, err.Error())
}
// 9. Create and save the service
now := time.Now().Unix()
service := &v1.Service{
Id: msg.ServiceId,
Domain: msg.Domain,
Owner: msg.Creator,
RootCapabilityCid: rootCapabilityCID,
Permissions: msg.RequestedPermissions,
Status: v1.ServiceStatus_SERVICE_STATUS_ACTIVE,
CreatedAt: now,
UpdatedAt: now,
}
err = ms.k.OrmDB.ServiceTable().Insert(ctx, service)
if err != nil {
return nil, errors.Wrap(types.ErrFailedToSaveService, err.Error())
}
// Emit typed event
sdkCtx := sdk.UnwrapSDKContext(ctx)
event := &types.EventServiceRegistered{
ServiceId: msg.ServiceId,
Domain: msg.Domain,
Owner: msg.Creator,
Endpoints: []string{}, // Can be populated if endpoints are provided
Metadata: "", // Can be populated with service metadata if needed
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventServiceRegistered")
}
return &types.MsgRegisterServiceResponse{
RootCapabilityCid: rootCapabilityCID,
ServiceId: msg.ServiceId,
}, nil
}
Regular → Executable
+382 -2
View File
@@ -2,10 +2,12 @@ package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/sonr-io/snrd/x/svc/types"
v1 "github.com/sonr-io/sonr/api/svc/v1"
"github.com/sonr-io/sonr/x/svc/types"
)
func TestParams(t *testing.T) {
@@ -50,7 +52,385 @@ func TestParams(t *testing.T) {
require.EqualValues(&tc.request.Params, r.Params)
}
})
}
}
func TestInitiateDomainVerification(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
testCases := []struct {
name string
domain string
creator string
expectError bool
errorMsg string
}{
{
name: "success - valid domain",
domain: "example.com",
creator: f.addrs[0].String(),
expectError: false,
},
{
name: "success - subdomain",
domain: "api.example.com",
creator: f.addrs[0].String(),
expectError: false,
},
{
name: "fail - empty domain",
domain: "",
creator: f.addrs[0].String(),
expectError: true,
errorMsg: "domain cannot be empty",
},
{
name: "fail - invalid domain format",
domain: "invalid domain with spaces",
creator: f.addrs[0].String(),
expectError: true,
errorMsg: "invalid domain format",
},
{
name: "fail - domain without dot",
domain: "localhost",
creator: f.addrs[0].String(),
expectError: true,
errorMsg: "must contain at least one dot",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
msg := &types.MsgInitiateDomainVerification{
Creator: tc.creator,
Domain: tc.domain,
}
resp, err := f.msgServer.InitiateDomainVerification(f.ctx, msg)
if tc.expectError {
require.Error(err)
require.Contains(err.Error(), tc.errorMsg)
require.Nil(resp)
} else {
require.NoError(err)
require.NotNil(resp)
require.NotEmpty(resp.VerificationToken)
require.Contains(resp.DnsInstruction, tc.domain)
require.Contains(resp.DnsInstruction, "sonr-verification=")
// Verify the domain verification was stored
verification, err := f.k.GetDomainVerification(f.ctx, tc.domain)
require.NoError(err)
require.Equal(tc.domain, verification.Domain)
require.Equal(tc.creator, verification.Owner)
require.Equal(resp.VerificationToken, verification.VerificationToken)
require.Equal(v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_PENDING, verification.Status)
require.Greater(verification.ExpiresAt, time.Now().Unix())
}
})
}
}
func TestInitiateDomainVerification_Duplicate(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
domain := "test.example.com"
creator := f.addrs[0].String()
// First verification should succeed
msg := &types.MsgInitiateDomainVerification{
Creator: creator,
Domain: domain,
}
resp1, err := f.msgServer.InitiateDomainVerification(f.ctx, msg)
require.NoError(err)
require.NotNil(resp1)
// Second verification attempt should return error (already exists and valid)
resp2, err := f.msgServer.InitiateDomainVerification(f.ctx, msg)
require.Error(err)
require.Contains(err.Error(), "already exists and is valid")
require.Nil(resp2)
}
func TestVerifyDomain(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
domain := "verify.example.com"
creator := f.addrs[0].String()
// First initiate domain verification
initMsg := &types.MsgInitiateDomainVerification{
Creator: creator,
Domain: domain,
}
_, err := f.msgServer.InitiateDomainVerification(f.ctx, initMsg)
require.NoError(err)
testCases := []struct {
name string
domain string
creator string
expectVerified bool
expectError bool
}{
{
name: "verify existing domain - will fail DNS lookup",
domain: domain,
creator: creator,
expectVerified: false,
expectError: false, // Error returned in response, not as gRPC error
},
{
name: "verify non-existent domain",
domain: "nonexistent.example.com",
creator: creator,
expectError: false, // Will return "not found" in response
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
msg := &types.MsgVerifyDomain{
Creator: tc.creator,
Domain: tc.domain,
}
resp, err := f.msgServer.VerifyDomain(f.ctx, msg)
if tc.expectError {
require.Error(err)
require.Nil(resp)
} else {
require.NoError(err)
require.NotNil(resp)
require.Equal(tc.expectVerified, resp.Verified)
require.NotEmpty(resp.Message)
if tc.domain == domain {
// For the initiated domain, check the verification status was updated
verification, err := f.k.GetDomainVerification(f.ctx, tc.domain)
require.NoError(err)
if resp.Verified {
require.Equal(v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED, verification.Status)
require.Greater(verification.VerifiedAt, int64(0))
} else {
require.Equal(v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_FAILED, verification.Status)
}
}
}
})
}
}
func TestRegisterService(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
// Setup: Create and manually verify a domain for testing
domain := "service.example.com"
creator := f.addrs[0].String()
// Insert a verified domain verification record directly
verification := &v1.DomainVerification{
Domain: domain,
Owner: creator,
VerificationToken: "test-token-12345",
Status: v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED,
ExpiresAt: time.Now().Unix() + 3600,
VerifiedAt: time.Now().Unix(),
}
err := f.k.OrmDB.DomainVerificationTable().Insert(f.ctx, verification)
require.NoError(err)
testCases := []struct {
name string
serviceId string
domain string
creator string
permissions []string
expectError bool
errorMsg string
}{
{
name: "success - register service with verified domain",
serviceId: "test-service-1",
domain: domain,
creator: creator,
permissions: []string{"register", "update"},
expectError: false,
},
{
name: "fail - empty service ID",
serviceId: "",
domain: domain,
creator: creator,
permissions: []string{"register"},
expectError: true,
errorMsg: "service ID cannot be empty",
},
{
name: "fail - unverified domain",
serviceId: "test-service-2",
domain: "unverified.example.com",
creator: creator,
permissions: []string{"register"},
expectError: true,
errorMsg: "domain is not verified",
},
{
name: "fail - duplicate service ID",
serviceId: "test-service-1", // Same as first successful test
domain: domain,
creator: creator,
permissions: []string{"register"},
expectError: true,
errorMsg: "service already exists",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
msg := &types.MsgRegisterService{
Creator: tc.creator,
ServiceId: tc.serviceId,
Domain: tc.domain,
RequestedPermissions: tc.permissions,
UcanDelegationChain: "", // Empty for testing - UCAN validation optional
}
resp, err := f.msgServer.RegisterService(f.ctx, msg)
if tc.expectError {
require.Error(err)
require.Contains(err.Error(), tc.errorMsg)
require.Nil(resp)
} else {
require.NoError(err)
require.NotNil(resp)
require.Equal(tc.serviceId, resp.ServiceId)
require.NotEmpty(resp.RootCapabilityCid)
// Verify the service was stored
service, err := f.k.OrmDB.ServiceTable().Get(f.ctx, tc.serviceId)
require.NoError(err)
require.Equal(tc.serviceId, service.Id)
require.Equal(tc.domain, service.Domain)
require.Equal(tc.creator, service.Owner)
require.Equal(tc.permissions, service.Permissions)
require.Equal(v1.ServiceStatus_SERVICE_STATUS_ACTIVE, service.Status)
require.Greater(service.CreatedAt, int64(0))
require.Greater(service.UpdatedAt, int64(0))
}
})
}
}
func TestRegisterService_DomainAlreadyBound(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
// Setup: Create and verify a domain
domain := "bound.example.com"
creator := f.addrs[0].String()
verification := &v1.DomainVerification{
Domain: domain,
Owner: creator,
VerificationToken: "test-token-bound",
Status: v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED,
ExpiresAt: time.Now().Unix() + 3600,
VerifiedAt: time.Now().Unix(),
}
err := f.k.OrmDB.DomainVerificationTable().Insert(f.ctx, verification)
require.NoError(err)
// Register first service successfully
msg1 := &types.MsgRegisterService{
Creator: creator,
ServiceId: "service-1",
Domain: domain,
RequestedPermissions: []string{"register"},
UcanDelegationChain: "", // Empty for testing - UCAN validation optional
}
resp1, err := f.msgServer.RegisterService(f.ctx, msg1)
require.NoError(err)
require.NotNil(resp1)
// Try to register second service with same domain - should fail
msg2 := &types.MsgRegisterService{
Creator: creator,
ServiceId: "service-2",
Domain: domain, // Same domain
RequestedPermissions: []string{"update"},
UcanDelegationChain: "", // Empty for testing - UCAN validation optional
}
resp2, err := f.msgServer.RegisterService(f.ctx, msg2)
require.Error(err)
require.Contains(err.Error(), "domain is already bound to another service")
require.Nil(resp2)
}
func TestDomainVerificationWorkflow(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
domain := "workflow.example.com"
creator := f.addrs[0].String()
// Step 1: Initiate domain verification
initiateMsg := &types.MsgInitiateDomainVerification{
Creator: creator,
Domain: domain,
}
initiateResp, err := f.msgServer.InitiateDomainVerification(f.ctx, initiateMsg)
require.NoError(err)
require.NotNil(initiateResp)
require.NotEmpty(initiateResp.VerificationToken)
// Step 2: Try to register service before verification - should fail
registerMsg := &types.MsgRegisterService{
Creator: creator,
ServiceId: "workflow-service",
Domain: domain,
RequestedPermissions: []string{"register"},
UcanDelegationChain: "", // Empty for testing - UCAN validation optional
}
_, err = f.msgServer.RegisterService(f.ctx, registerMsg)
require.Error(err)
require.Contains(err.Error(), "domain is not verified")
// Step 3: Manually mark domain as verified (simulating successful DNS verification)
verification, err := f.k.GetDomainVerification(f.ctx, domain)
require.NoError(err)
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
verification.VerifiedAt = time.Now().Unix()
err = f.k.OrmDB.DomainVerificationTable().Update(f.ctx, verification)
require.NoError(err)
// Step 4: Now service registration should succeed
registerResp, err := f.msgServer.RegisterService(f.ctx, registerMsg)
require.NoError(err)
require.NotNil(registerResp)
require.Equal("workflow-service", registerResp.ServiceId)
// Step 5: Verify the complete workflow
service, err := f.k.OrmDB.ServiceTable().Get(f.ctx, "workflow-service")
require.NoError(err)
require.Equal(domain, service.Domain)
require.Equal(creator, service.Owner)
}
+270
View File
@@ -0,0 +1,270 @@
package keeper
import (
"context"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
apiv1 "github.com/sonr-io/sonr/api/svc/v1"
"github.com/sonr-io/sonr/x/svc/types"
)
// SetServiceOIDCConfig stores the OIDC configuration for a service
func (k Keeper) SetServiceOIDCConfig(ctx context.Context, config *types.ServiceOIDCConfig) error {
// Validate service exists
service, err := k.OrmDB.ServiceTable().Get(ctx, config.ServiceId)
if err != nil {
return fmt.Errorf("service not found: %s", config.ServiceId)
}
// Verify domain matches issuer
if !k.validateIssuerDomain(config.Issuer, service.Domain) {
return fmt.Errorf("issuer must match verified domain: %s", service.Domain)
}
// Convert to API type and store
apiConfig := convertTypesOIDCConfigToAPI(config)
return k.OrmDB.ServiceOIDCConfigTable().Save(ctx, apiConfig)
}
// GetServiceOIDCConfig retrieves the OIDC configuration for a service
func (k Keeper) GetServiceOIDCConfig(ctx context.Context, serviceID string) (*types.ServiceOIDCConfig, error) {
apiConfig, err := k.OrmDB.ServiceOIDCConfigTable().Get(ctx, serviceID)
if err != nil {
return nil, err
}
return convertAPIOIDCConfigToTypes(apiConfig), nil
}
// SetServiceJWKS stores the JWKS for a service
func (k Keeper) SetServiceJWKS(ctx context.Context, jwks *types.ServiceJWKS) error {
// Validate service exists
_, err := k.OrmDB.ServiceTable().Get(ctx, jwks.ServiceId)
if err != nil {
return fmt.Errorf("service not found: %s", jwks.ServiceId)
}
// Convert to API type and store
apiJWKS := convertTypesJWKSToAPI(jwks)
return k.OrmDB.ServiceJWKSTable().Save(ctx, apiJWKS)
}
// GetServiceJWKS retrieves the JWKS for a service
func (k Keeper) GetServiceJWKS(ctx context.Context, serviceID string) (*types.ServiceJWKS, error) {
apiJWKS, err := k.OrmDB.ServiceJWKSTable().Get(ctx, serviceID)
if err != nil {
return nil, err
}
return convertAPIJWKSToTypes(apiJWKS), nil
}
// validateIssuerDomain ensures the issuer URL matches the verified domain
func (k Keeper) validateIssuerDomain(issuer, domain string) bool {
// Simple validation: issuer should contain the domain
// In production, parse URL and validate hostname
return true // Placeholder for now
}
// CreateDefaultOIDCConfig creates a default OIDC configuration for a service
func (k Keeper) CreateDefaultOIDCConfig(ctx context.Context, serviceID string, domain string) (*types.ServiceOIDCConfig, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
config := &types.ServiceOIDCConfig{
ServiceId: serviceID,
Issuer: fmt.Sprintf("https://%s", domain),
AuthorizationEndpoint: fmt.Sprintf("https://%s/oauth/authorize", domain),
TokenEndpoint: fmt.Sprintf("https://%s/oauth/token", domain),
JwksUri: fmt.Sprintf("https://%s/.well-known/jwks.json", domain),
UserinfoEndpoint: fmt.Sprintf("https://%s/oauth/userinfo", domain),
ScopesSupported: []string{
"openid",
"profile",
"email",
"offline_access",
},
ResponseTypesSupported: []string{
"code",
"token",
"id_token",
"code token",
"code id_token",
"token id_token",
"code token id_token",
},
GrantTypesSupported: []string{
"authorization_code",
"implicit",
"refresh_token",
"client_credentials",
},
IdTokenSigningAlgValuesSupported: []string{
"RS256",
"ES256",
},
SubjectTypesSupported: []string{
"public",
"pairwise",
},
TokenEndpointAuthMethodsSupported: []string{
"client_secret_basic",
"client_secret_post",
"none",
},
ClaimsSupported: []string{
"sub",
"iss",
"aud",
"exp",
"iat",
"nonce",
"email",
"email_verified",
"name",
"preferred_username",
"picture",
"did",
"wallet_address",
},
ResponseModesSupported: []string{
"query",
"fragment",
"form_post",
},
Metadata: map[string]string{
"service_id": serviceID,
"blockchain": "sonr",
"chain_id": sdkCtx.ChainID(),
},
CreatedAt: sdkCtx.BlockTime().Unix(),
UpdatedAt: sdkCtx.BlockTime().Unix(),
}
// Store the config
err := k.SetServiceOIDCConfig(ctx, config)
if err != nil {
return nil, err
}
return config, nil
}
// CreateDefaultJWKS creates a default JWKS for a service
func (k Keeper) CreateDefaultJWKS(ctx context.Context, serviceID string) (*types.ServiceJWKS, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
// For now, create an empty JWKS
// In production, this would generate or retrieve actual keys
jwks := &types.ServiceJWKS{
ServiceId: serviceID,
Keys: []*types.JWK{},
RotatedAt: sdkCtx.BlockTime().Unix(),
}
// Store the JWKS
err := k.SetServiceJWKS(ctx, jwks)
if err != nil {
return nil, err
}
return jwks, nil
}
// Conversion functions between API and Types
func convertTypesOIDCConfigToAPI(config *types.ServiceOIDCConfig) *apiv1.ServiceOIDCConfig {
return &apiv1.ServiceOIDCConfig{
ServiceId: config.ServiceId,
Issuer: config.Issuer,
AuthorizationEndpoint: config.AuthorizationEndpoint,
TokenEndpoint: config.TokenEndpoint,
JwksUri: config.JwksUri,
UserinfoEndpoint: config.UserinfoEndpoint,
ScopesSupported: config.ScopesSupported,
ResponseTypesSupported: config.ResponseTypesSupported,
GrantTypesSupported: config.GrantTypesSupported,
IdTokenSigningAlgValuesSupported: config.IdTokenSigningAlgValuesSupported,
SubjectTypesSupported: config.SubjectTypesSupported,
TokenEndpointAuthMethodsSupported: config.TokenEndpointAuthMethodsSupported,
ClaimsSupported: config.ClaimsSupported,
ResponseModesSupported: config.ResponseModesSupported,
Metadata: config.Metadata,
CreatedAt: config.CreatedAt,
UpdatedAt: config.UpdatedAt,
}
}
func convertAPIOIDCConfigToTypes(config *apiv1.ServiceOIDCConfig) *types.ServiceOIDCConfig {
return &types.ServiceOIDCConfig{
ServiceId: config.ServiceId,
Issuer: config.Issuer,
AuthorizationEndpoint: config.AuthorizationEndpoint,
TokenEndpoint: config.TokenEndpoint,
JwksUri: config.JwksUri,
UserinfoEndpoint: config.UserinfoEndpoint,
ScopesSupported: config.ScopesSupported,
ResponseTypesSupported: config.ResponseTypesSupported,
GrantTypesSupported: config.GrantTypesSupported,
IdTokenSigningAlgValuesSupported: config.IdTokenSigningAlgValuesSupported,
SubjectTypesSupported: config.SubjectTypesSupported,
TokenEndpointAuthMethodsSupported: config.TokenEndpointAuthMethodsSupported,
ClaimsSupported: config.ClaimsSupported,
ResponseModesSupported: config.ResponseModesSupported,
Metadata: config.Metadata,
CreatedAt: config.CreatedAt,
UpdatedAt: config.UpdatedAt,
}
}
func convertTypesJWKSToAPI(jwks *types.ServiceJWKS) *apiv1.ServiceJWKS {
apiKeys := make([]*apiv1.JWK, len(jwks.Keys))
for i, key := range jwks.Keys {
apiKeys[i] = convertTypesJWKToAPI(key)
}
return &apiv1.ServiceJWKS{
ServiceId: jwks.ServiceId,
Keys: apiKeys,
RotatedAt: jwks.RotatedAt,
}
}
func convertAPIJWKSToTypes(jwks *apiv1.ServiceJWKS) *types.ServiceJWKS {
typesKeys := make([]*types.JWK, len(jwks.Keys))
for i, key := range jwks.Keys {
typesKeys[i] = convertAPIJWKToTypes(key)
}
return &types.ServiceJWKS{
ServiceId: jwks.ServiceId,
Keys: typesKeys,
RotatedAt: jwks.RotatedAt,
}
}
func convertTypesJWKToAPI(key *types.JWK) *apiv1.JWK {
return &apiv1.JWK{
Kty: key.Kty,
Use: key.Use,
Kid: key.Kid,
Alg: key.Alg,
N: key.N,
E: key.E,
Crv: key.Crv,
X: key.X,
Y: key.Y,
}
}
func convertAPIJWKToTypes(key *apiv1.JWK) *types.JWK {
return &types.JWK{
Kty: key.Kty,
Use: key.Use,
Kid: key.Kid,
Alg: key.Alg,
N: key.N,
E: key.E,
Crv: key.Crv,
X: key.X,
Y: key.Y,
}
}
+185
View File
@@ -0,0 +1,185 @@
package keeper_test
import (
"testing"
"time"
apiv1 "github.com/sonr-io/sonr/api/svc/v1"
"github.com/stretchr/testify/require"
)
func TestDomainVerificationORM(t *testing.T) {
f := SetupTest(t)
dt := f.k.OrmDB.DomainVerificationTable()
domain := "example.com"
owner := "cosmos1abc123"
token := "verification-token-12345"
now := time.Now().Unix()
// Test Insert
err := dt.Insert(f.ctx, &apiv1.DomainVerification{
Domain: domain,
Owner: owner,
VerificationToken: token,
Status: apiv1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_PENDING,
ExpiresAt: now + 3600, // 1 hour from now
VerifiedAt: 0, // Not verified yet
})
require.NoError(t, err)
// Test Has
exists, err := dt.Has(f.ctx, domain)
require.NoError(t, err)
require.True(t, exists)
// Test Get
res, err := dt.Get(f.ctx, domain)
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, domain, res.Domain)
require.Equal(t, owner, res.Owner)
require.Equal(t, token, res.VerificationToken)
require.Equal(t, apiv1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_PENDING, res.Status)
require.Equal(t, now+3600, res.ExpiresAt)
require.Equal(t, int64(0), res.VerifiedAt)
// Test Update
res.Status = apiv1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
res.VerifiedAt = now
err = dt.Update(f.ctx, res)
require.NoError(t, err)
// Verify update
updated, err := dt.Get(f.ctx, domain)
require.NoError(t, err)
require.Equal(
t,
apiv1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED,
updated.Status,
)
require.Equal(t, now, updated.VerifiedAt)
}
func TestServiceORM(t *testing.T) {
f := SetupTest(t)
st := f.k.OrmDB.ServiceTable()
serviceID := "service-123"
domain := "api.example.com"
owner := "cosmos1def456"
capabilityCID := "QmServiceCapability123"
now := time.Now().Unix()
// Test Insert
err := st.Insert(f.ctx, &apiv1.Service{
Id: serviceID,
Domain: domain,
Owner: owner,
RootCapabilityCid: capabilityCID,
Permissions: []string{"register", "update"},
Status: apiv1.ServiceStatus_SERVICE_STATUS_ACTIVE,
CreatedAt: now,
UpdatedAt: now,
})
require.NoError(t, err)
// Test Has
exists, err := st.Has(f.ctx, serviceID)
require.NoError(t, err)
require.True(t, exists)
// Test Get
res, err := st.Get(f.ctx, serviceID)
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, serviceID, res.Id)
require.Equal(t, domain, res.Domain)
require.Equal(t, owner, res.Owner)
require.Equal(t, capabilityCID, res.RootCapabilityCid)
require.Equal(t, []string{"register", "update"}, res.Permissions)
require.Equal(t, apiv1.ServiceStatus_SERVICE_STATUS_ACTIVE, res.Status)
require.Equal(t, now, res.CreatedAt)
require.Equal(t, now, res.UpdatedAt)
// Test Update
res.Status = apiv1.ServiceStatus_SERVICE_STATUS_SUSPENDED
res.UpdatedAt = now + 100
err = st.Update(f.ctx, res)
require.NoError(t, err)
// Verify update
updated, err := st.Get(f.ctx, serviceID)
require.NoError(t, err)
require.Equal(t, apiv1.ServiceStatus_SERVICE_STATUS_SUSPENDED, updated.Status)
require.Equal(t, now+100, updated.UpdatedAt)
}
func TestServiceIndexQueries(t *testing.T) {
f := SetupTest(t)
st := f.k.OrmDB.ServiceTable()
owner := "cosmos1test123"
domain := "test.example.com"
now := time.Now().Unix()
// Insert test services
services := []*apiv1.Service{
{
Id: "service-1",
Domain: domain,
Owner: owner,
RootCapabilityCid: "QmCap1",
Permissions: []string{"register"},
Status: apiv1.ServiceStatus_SERVICE_STATUS_ACTIVE,
CreatedAt: now,
UpdatedAt: now,
},
{
Id: "service-2",
Domain: "other.example.com",
Owner: owner,
RootCapabilityCid: "QmCap2",
Permissions: []string{"update"},
Status: apiv1.ServiceStatus_SERVICE_STATUS_SUSPENDED,
CreatedAt: now + 100,
UpdatedAt: now + 100,
},
}
for _, service := range services {
err := st.Insert(f.ctx, service)
require.NoError(t, err)
}
// Test query by owner index
ownerKey := apiv1.ServiceOwnerIndexKey{}.WithOwner(owner)
iter, err := st.List(f.ctx, ownerKey)
require.NoError(t, err)
var ownerServices []*apiv1.Service
for iter.Next() {
service, errb := iter.Value()
require.NoError(t, errb)
ownerServices = append(ownerServices, service)
}
iter.Close()
require.Len(t, ownerServices, 2)
// Test query by domain index
domainKey := apiv1.ServiceDomainIndexKey{}.WithDomain(domain)
iter, err = st.List(f.ctx, domainKey)
require.NoError(t, err)
var domainServices []*apiv1.Service
for iter.Next() {
service, err := iter.Value()
require.NoError(t, err)
domainServices = append(domainServices, service)
}
iter.Close()
require.Len(t, domainServices, 1)
require.Equal(t, "service-1", domainServices[0].Id)
}
+310
View File
@@ -0,0 +1,310 @@
package keeper
import (
"context"
"fmt"
"github.com/sonr-io/sonr/crypto/keys"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/sonr/x/svc/types"
)
// PermissionValidator wraps UCAN verifier for Service-specific permission validation
type PermissionValidator struct {
verifier *ucan.Verifier
keeper Keeper
permissions *types.UCANPermissionRegistry
}
// NewPermissionValidator creates a new Service permission validator
func NewPermissionValidator(keeper Keeper) *PermissionValidator {
didResolver := &ServiceDIDResolver{keeper: keeper}
verifier := ucan.NewVerifier(didResolver)
return &PermissionValidator{
verifier: verifier,
keeper: keeper,
permissions: types.NewUCANPermissionRegistry(),
}
}
// ValidatePermission validates UCAN token for Service operation
func (pv *PermissionValidator) ValidatePermission(
ctx context.Context,
tokenString string,
serviceID string,
operation types.ServiceOperation,
) error {
// Get required UCAN capabilities for the operation
capabilities, err := pv.permissions.GetRequiredUCANCapabilities(operation)
if err != nil {
return fmt.Errorf("failed to get required UCAN capabilities: %w", err)
}
// Build resource URI for Service
resourceURI := pv.buildResourceURI(serviceID)
// Verify UCAN token grants required capabilities
_, err = pv.verifier.VerifyCapability(
ctx,
tokenString,
resourceURI,
capabilities,
)
if err != nil {
return fmt.Errorf("UCAN validation failed: %w", err)
}
return nil
}
// ValidateDomainBoundPermission validates UCAN token for domain-bound operations
func (pv *PermissionValidator) ValidateDomainBoundPermission(
ctx context.Context,
tokenString string,
domain string,
serviceID string,
operation types.ServiceOperation,
) error {
// Get required UCAN capabilities for the operation
capabilities, err := pv.permissions.GetRequiredUCANCapabilities(operation)
if err != nil {
return fmt.Errorf("failed to get required UCAN capabilities: %w", err)
}
// Build domain resource URI
resourceURI := pv.buildDomainResourceURI(domain)
// Verify UCAN token with domain-bound validation
token, err := pv.verifier.VerifyCapability(
ctx,
tokenString,
resourceURI,
capabilities,
)
if err != nil {
return fmt.Errorf("UCAN validation failed: %w", err)
}
// Additional domain-bound validation
if err := pv.validateDomainBoundCaveat(token, domain, serviceID); err != nil {
return fmt.Errorf("domain-bound validation failed: %w", err)
}
return nil
}
// ValidateDomainVerificationPermission validates UCAN token for domain verification
func (pv *PermissionValidator) ValidateDomainVerificationPermission(
ctx context.Context,
tokenString string,
domain string,
verificationMethod string,
operation types.ServiceOperation,
) error {
// Get required UCAN capabilities for the operation
capabilities, err := pv.permissions.GetRequiredUCANCapabilities(operation)
if err != nil {
return fmt.Errorf("failed to get required UCAN capabilities: %w", err)
}
// Build domain verification resource URI
resourceURI := types.CreateDomainVerificationURI(domain, verificationMethod)
// Verify UCAN token
token, err := pv.verifier.VerifyCapability(
ctx,
tokenString,
resourceURI,
capabilities,
)
if err != nil {
return fmt.Errorf("UCAN validation failed: %w", err)
}
// Additional domain verification validation
if err := pv.validateDomainVerification(token, domain, verificationMethod); err != nil {
return fmt.Errorf("domain verification validation failed: %w", err)
}
return nil
}
// VerifyDelegationChain validates complete UCAN delegation chain
func (pv *PermissionValidator) VerifyDelegationChain(
ctx context.Context,
tokenString string,
) error {
return pv.verifier.VerifyDelegationChain(ctx, tokenString)
}
// Internal validation methods
// validateDomainBoundCaveat validates that the token has proper domain binding
func (pv *PermissionValidator) validateDomainBoundCaveat(
token *ucan.Token,
domain string,
serviceID string,
) error {
// Check each attenuation for domain-bound resources
for _, att := range token.Attenuations {
if simpleResource, ok := att.Resource.(*ucan.SimpleResource); ok {
if simpleResource.Scheme == "domain" && simpleResource.Value == domain {
// Found matching domain resource
return nil
}
}
}
return fmt.Errorf("no matching domain-bound attenuation found for domain %s", domain)
}
// validateDomainVerification validates domain verification capability
func (pv *PermissionValidator) validateDomainVerification(
token *ucan.Token,
domain string,
verificationMethod string,
) error {
// Find the relevant attenuation for this domain
for _, att := range token.Attenuations {
if err := types.ValidateDomainVerificationCapability(att.Capability, domain, verificationMethod); err == nil {
return nil
}
}
return fmt.Errorf("no valid domain verification capability found for domain %s", domain)
}
// Helper methods
// buildResourceURI constructs Service resource URI
func (pv *PermissionValidator) buildResourceURI(serviceID string) string {
return fmt.Sprintf("svc:%s", serviceID)
}
// buildDomainResourceURI constructs domain resource URI
func (pv *PermissionValidator) buildDomainResourceURI(domain string) string {
return fmt.Sprintf("domain:%s", domain)
}
// CreateAttenuation creates a UCAN attenuation for Service operations
func (pv *PermissionValidator) CreateAttenuation(
actions []string,
serviceID string,
caveats []string,
) ucan.Attenuation {
return pv.permissions.CreateServiceAttenuation(actions, serviceID, caveats)
}
// CreateDomainBoundAttenuation creates a domain-bound UCAN attenuation
func (pv *PermissionValidator) CreateDomainBoundAttenuation(
actions []string,
domain string,
serviceID string,
) ucan.Attenuation {
return pv.permissions.CreateDomainBoundAttenuation(actions, domain, serviceID)
}
// CreateRateLimitedAttenuation creates a rate-limited UCAN attenuation
func (pv *PermissionValidator) CreateRateLimitedAttenuation(
actions []string,
serviceID string,
rateLimit uint64,
windowSeconds uint64,
) ucan.Attenuation {
return pv.permissions.CreateRateLimitedAttenuation(actions, serviceID, rateLimit, windowSeconds)
}
// ServiceDIDResolver implements ucan.DIDResolver for Service module
type ServiceDIDResolver struct {
keeper Keeper
}
// ResolveDIDKey resolves DID to public key for UCAN verification
func (r *ServiceDIDResolver) ResolveDIDKey(ctx context.Context, did string) (keys.DID, error) {
// Get the DID document from the keeper
didDoc, err := r.keeper.didKeeper.GetDIDDocument(ctx, did)
if err != nil {
return keys.DID{}, fmt.Errorf("failed to get DID document: %w", err)
}
if didDoc == nil {
return keys.DID{}, types.ErrInvalidOwnerDID
}
// Parse the DID string into a keys.DID
// This assumes the DID keeper can provide the public key information
return keys.Parse(did)
}
// Gasless transaction support
// SupportsGaslessTransaction checks if a UCAN token supports gasless transactions
func (pv *PermissionValidator) SupportsGaslessTransaction(
ctx context.Context,
tokenString string,
serviceID string,
operation types.ServiceOperation,
) (bool, uint64, error) {
// Parse and verify the token
token, err := pv.verifier.VerifyToken(ctx, tokenString)
if err != nil {
return false, 0, fmt.Errorf("token verification failed: %w", err)
}
resourceURI := pv.buildResourceURI(serviceID)
// Check each attenuation for gasless support
for _, att := range token.Attenuations {
if att.Resource.GetURI() == resourceURI {
// Check if capability supports gasless transactions
if gaslessCapability, ok := att.Capability.(*ucan.GaslessCapability); ok {
if gaslessCapability.SupportsGasless() {
// Verify the capability grants the required operation
capabilities, err := pv.permissions.GetRequiredUCANCapabilities(operation)
if err != nil {
continue
}
if gaslessCapability.Grants(capabilities) {
return true, gaslessCapability.GetGasLimit(), nil
}
}
}
}
}
return false, 0, nil
}
// ValidateRateLimit checks if a UCAN token has rate limiting and if it's within limits
func (pv *PermissionValidator) ValidateRateLimit(
ctx context.Context,
tokenString string,
serviceID string,
) (bool, uint64, uint64, error) {
// Parse and verify the token
token, err := pv.verifier.VerifyToken(ctx, tokenString)
if err != nil {
return false, 0, 0, fmt.Errorf("token verification failed: %w", err)
}
resourceURI := pv.buildResourceURI(serviceID)
// Check each attenuation for rate limiting
// Rate limiting would typically be implemented with custom capability types
// or through external state management
for _, att := range token.Attenuations {
if att.Resource.GetURI() == resourceURI {
// Check if this is a gasless capability with limits
if gaslessCapability, ok := att.Capability.(*ucan.GaslessCapability); ok {
if gaslessCapability.AllowGasless && gaslessCapability.GasLimit > 0 {
// Use gas limit as a proxy for rate limiting
return true, gaslessCapability.GasLimit, 60, nil // 60 second window
}
}
}
}
return false, 0, 0, nil
}
Regular → Executable
+276 -10
View File
@@ -2,12 +2,28 @@ package keeper
import (
"context"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/snrd/x/svc/types"
apiv1 "github.com/sonr-io/sonr/api/svc/v1"
"github.com/sonr-io/sonr/x/svc/types"
)
// convertV1ServiceToTypes converts a v1.Service to types.Service
func convertV1ServiceToTypes(v1Service *apiv1.Service) *types.Service {
return &types.Service{
Id: v1Service.Id,
Domain: v1Service.Domain,
Owner: v1Service.Owner,
RootCapabilityCid: v1Service.RootCapabilityCid,
Permissions: v1Service.Permissions,
Status: types.ServiceStatus(v1Service.Status),
CreatedAt: v1Service.CreatedAt,
UpdatedAt: v1Service.UpdatedAt,
}
}
var _ types.QueryServer = Querier{}
type Querier struct {
@@ -18,7 +34,10 @@ func NewQuerier(keeper Keeper) Querier {
return Querier{Keeper: keeper}
}
func (k Querier) Params(c context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) {
func (k Querier) Params(
c context.Context,
req *types.QueryParamsRequest,
) (*types.QueryParamsResponse, error) {
ctx := sdk.UnwrapSDKContext(c)
p, err := k.Keeper.Params.Get(ctx)
@@ -29,14 +48,261 @@ func (k Querier) Params(c context.Context, req *types.QueryParamsRequest) (*type
return &types.QueryParamsResponse{Params: &p}, nil
}
// OriginExists implements types.QueryServer.
func (k Querier) OriginExists(goCtx context.Context, req *types.QueryOriginExistsRequest) (*types.QueryOriginExistsResponse, error) {
// ctx := sdk.UnwrapSDKContext(goCtx)
return &types.QueryOriginExistsResponse{}, nil
// DomainVerification implements types.QueryServer.
func (k Querier) DomainVerification(
goCtx context.Context,
req *types.QueryDomainVerificationRequest,
) (*types.QueryDomainVerificationResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
if req.Domain == "" {
return nil, fmt.Errorf("domain cannot be empty")
}
// Get domain verification from ORM
verification, err := k.Keeper.OrmDB.DomainVerificationTable().Get(ctx, req.Domain)
if err != nil {
return nil, fmt.Errorf("domain verification not found: %w", err)
}
// Convert v1.DomainVerification to types.DomainVerification
typesVerification := &types.DomainVerification{
Domain: verification.Domain,
Owner: verification.Owner,
VerificationToken: verification.VerificationToken,
Status: types.DomainVerificationStatus(verification.Status),
ExpiresAt: verification.ExpiresAt,
}
return &types.QueryDomainVerificationResponse{
DomainVerification: typesVerification,
}, nil
}
// ResolveOrigin implements types.QueryServer.
func (k Querier) ResolveOrigin(goCtx context.Context, req *types.QueryResolveOriginRequest) (*types.QueryResolveOriginResponse, error) {
// ctx := sdk.UnwrapSDKContext(goCtx)
return &types.QueryResolveOriginResponse{}, nil
// Service implements types.QueryServer.
func (k Querier) Service(
goCtx context.Context,
req *types.QueryServiceRequest,
) (*types.QueryServiceResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
if req.ServiceId == "" {
return nil, fmt.Errorf("service_id cannot be empty")
}
// Get service from ORM
service, err := k.Keeper.OrmDB.ServiceTable().Get(ctx, req.ServiceId)
if err != nil {
return nil, fmt.Errorf("service not found: %w", err)
}
return &types.QueryServiceResponse{
Service: convertV1ServiceToTypes(service),
}, nil
}
// ServicesByOwner implements types.QueryServer.
func (k Querier) ServicesByOwner(
goCtx context.Context,
req *types.QueryServicesByOwnerRequest,
) (*types.QueryServicesByOwnerResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
if req.Owner == "" {
return nil, fmt.Errorf("owner cannot be empty")
}
// Create index key for owner
ownerKey := apiv1.ServiceOwnerIndexKey{}.WithOwner(req.Owner)
// List services by owner
iter, err := k.Keeper.OrmDB.ServiceTable().List(ctx, ownerKey)
if err != nil {
return nil, fmt.Errorf("failed to list services by owner: %w", err)
}
defer iter.Close()
var services []*types.Service
for iter.Next() {
service, err := iter.Value()
if err != nil {
return nil, fmt.Errorf("failed to get service value: %w", err)
}
services = append(services, convertV1ServiceToTypes(service))
}
return &types.QueryServicesByOwnerResponse{
Services: services,
}, nil
}
// ServicesByDomain implements types.QueryServer.
func (k Querier) ServicesByDomain(
goCtx context.Context,
req *types.QueryServicesByDomainRequest,
) (*types.QueryServicesByDomainResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
if req.Domain == "" {
return nil, fmt.Errorf("domain cannot be empty")
}
// Create index key for domain
domainKey := apiv1.ServiceDomainIndexKey{}.WithDomain(req.Domain)
// List services by domain
iter, err := k.Keeper.OrmDB.ServiceTable().List(ctx, domainKey)
if err != nil {
return nil, fmt.Errorf("failed to list services by domain: %w", err)
}
defer iter.Close()
var services []*types.Service
for iter.Next() {
service, err := iter.Value()
if err != nil {
return nil, fmt.Errorf("failed to get service value: %w", err)
}
services = append(services, convertV1ServiceToTypes(service))
}
return &types.QueryServicesByDomainResponse{
Services: services,
}, nil
}
// ServiceOIDCDiscovery implements types.QueryServer.
func (k Querier) ServiceOIDCDiscovery(goCtx context.Context, req *types.QueryServiceOIDCDiscoveryRequest) (*types.QueryServiceOIDCDiscoveryResponse, error) {
if req == nil || req.ServiceId == "" {
return nil, types.ErrInvalidServiceID
}
// Get service to verify it exists and is active
service, err := k.Keeper.OrmDB.ServiceTable().Get(goCtx, req.ServiceId)
if err != nil {
return nil, types.ErrServiceNotFound
}
if service.Status != apiv1.ServiceStatus_SERVICE_STATUS_ACTIVE {
return nil, types.ErrServiceNotActive
}
// Get OIDC config
config, err := k.Keeper.GetServiceOIDCConfig(goCtx, req.ServiceId)
if err != nil {
// If no config exists, create default based on verified domain
config, err = k.Keeper.CreateDefaultOIDCConfig(goCtx, req.ServiceId, service.Domain)
if err != nil {
return nil, err
}
}
// Build OIDC discovery response according to spec
return &types.QueryServiceOIDCDiscoveryResponse{
Issuer: config.Issuer,
AuthorizationEndpoint: config.AuthorizationEndpoint,
TokenEndpoint: config.TokenEndpoint,
JwksUri: config.JwksUri,
UserinfoEndpoint: config.UserinfoEndpoint,
RegistrationEndpoint: fmt.Sprintf("https://%s/oauth/register", service.Domain),
ScopesSupported: config.ScopesSupported,
ResponseTypesSupported: config.ResponseTypesSupported,
GrantTypesSupported: config.GrantTypesSupported,
IdTokenSigningAlgValuesSupported: config.IdTokenSigningAlgValuesSupported,
SubjectTypesSupported: config.SubjectTypesSupported,
TokenEndpointAuthMethodsSupported: config.TokenEndpointAuthMethodsSupported,
ClaimsSupported: config.ClaimsSupported,
ResponseModesSupported: config.ResponseModesSupported,
ServiceDocumentation: fmt.Sprintf("https://%s/docs", service.Domain),
UiLocalesSupported: []string{"en-US"},
ClaimsLocalesSupported: []string{"en-US"},
RequestParameterSupported: true,
RequestUriParameterSupported: true,
RequireRequestUriRegistration: false,
OpPolicyUri: fmt.Sprintf("https://%s/policy", service.Domain),
OpTosUri: fmt.Sprintf("https://%s/terms", service.Domain),
}, nil
}
// ServiceOIDCJWKS implements types.QueryServer.
func (k Querier) ServiceOIDCJWKS(goCtx context.Context, req *types.QueryServiceOIDCJWKSRequest) (*types.QueryServiceOIDCJWKSResponse, error) {
if req == nil || req.ServiceId == "" {
return nil, types.ErrInvalidServiceID
}
// Get service to verify it exists
service, err := k.Keeper.OrmDB.ServiceTable().Get(goCtx, req.ServiceId)
if err != nil {
return nil, types.ErrServiceNotFound
}
if service.Status != apiv1.ServiceStatus_SERVICE_STATUS_ACTIVE {
return nil, types.ErrServiceNotActive
}
// Get JWKS
jwks, err := k.Keeper.GetServiceJWKS(goCtx, req.ServiceId)
if err != nil {
// If no JWKS exists, create default
jwks, err = k.Keeper.CreateDefaultJWKS(goCtx, req.ServiceId)
if err != nil {
return nil, err
}
}
// Return JWKS response
return &types.QueryServiceOIDCJWKSResponse{
Keys: jwks.Keys,
}, nil
}
// ServiceOIDCMetadata implements types.QueryServer.
func (k Querier) ServiceOIDCMetadata(goCtx context.Context, req *types.QueryServiceOIDCMetadataRequest) (*types.QueryServiceOIDCMetadataResponse, error) {
if req == nil || req.ServiceId == "" {
return nil, types.ErrInvalidServiceID
}
// Get service
service, err := k.Keeper.OrmDB.ServiceTable().Get(goCtx, req.ServiceId)
if err != nil {
return nil, types.ErrServiceNotFound
}
// Get OIDC config
config, err := k.Keeper.GetServiceOIDCConfig(goCtx, req.ServiceId)
if err != nil {
// If no config exists, create default based on verified domain
config, err = k.Keeper.CreateDefaultOIDCConfig(goCtx, req.ServiceId, service.Domain)
if err != nil {
return nil, err
}
}
// Convert service status
var serviceStatus types.ServiceStatus
switch service.Status {
case apiv1.ServiceStatus_SERVICE_STATUS_ACTIVE:
serviceStatus = types.ServiceStatus_SERVICE_STATUS_ACTIVE
case apiv1.ServiceStatus_SERVICE_STATUS_SUSPENDED:
serviceStatus = types.ServiceStatus_SERVICE_STATUS_SUSPENDED
case apiv1.ServiceStatus_SERVICE_STATUS_REVOKED:
serviceStatus = types.ServiceStatus_SERVICE_STATUS_REVOKED
default:
serviceStatus = types.ServiceStatus_SERVICE_STATUS_ACTIVE
}
// Build metadata response
return &types.QueryServiceOIDCMetadataResponse{
Config: config,
VerifiedDomain: service.Domain,
ServiceStatus: serviceStatus,
Metadata: map[string]string{
"service_id": service.Id,
"owner": service.Owner,
"created_at": fmt.Sprintf("%d", service.CreatedAt),
"updated_at": fmt.Sprintf("%d", service.UpdatedAt),
"ucan_root_cid": service.RootCapabilityCid,
"oidc_enabled": "true",
},
}, nil
}
+191
View File
@@ -0,0 +1,191 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/x/svc/types"
)
func TestQueryDomainVerification(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
// First create a domain verification
_, err := f.k.InitiateDomainVerification(f.ctx, "example.com", "idx1test")
require.NoError(err)
// Query the domain verification
resp, err := f.queryServer.DomainVerification(f.ctx, &types.QueryDomainVerificationRequest{
Domain: "example.com",
})
require.NoError(err)
require.NotNil(resp.DomainVerification)
require.Equal("example.com", resp.DomainVerification.Domain)
require.Equal("idx1test", resp.DomainVerification.Owner)
require.NotEmpty(resp.DomainVerification.VerificationToken)
}
func TestQueryService(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
// First register a service (need domain verified first)
_, err := f.k.InitiateDomainVerification(f.ctx, "example.com", "idx1test")
require.NoError(err)
err = f.k.SetDomainVerified(f.ctx, "example.com")
require.NoError(err)
registerResp, err := f.msgServer.RegisterService(f.ctx, &types.MsgRegisterService{
Creator: "idx1test",
ServiceId: "test-service",
Domain: "example.com",
RequestedPermissions: []string{"register", "update"},
UcanDelegationChain: "",
})
require.NoError(err)
require.NotNil(registerResp)
// Query the service
resp, err := f.queryServer.Service(f.ctx, &types.QueryServiceRequest{
ServiceId: "test-service",
})
require.NoError(err)
require.NotNil(resp.Service)
require.Equal("test-service", resp.Service.Id)
require.Equal("example.com", resp.Service.Domain)
require.Equal("idx1test", resp.Service.Owner)
require.Contains(resp.Service.Permissions, "register")
require.Contains(resp.Service.Permissions, "update")
}
func TestQueryServicesByOwner(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
// Setup verified domain and register multiple services
_, err := f.k.InitiateDomainVerification(f.ctx, "example.com", "idx1test")
require.NoError(err)
err = f.k.SetDomainVerified(f.ctx, "example.com")
require.NoError(err)
_, err = f.k.InitiateDomainVerification(f.ctx, "test.org", "idx1test")
require.NoError(err)
err = f.k.SetDomainVerified(f.ctx, "test.org")
require.NoError(err)
// Register first service
_, err = f.msgServer.RegisterService(f.ctx, &types.MsgRegisterService{
Creator: "idx1test",
ServiceId: "service1",
Domain: "example.com",
RequestedPermissions: []string{"register"},
UcanDelegationChain: "",
})
require.NoError(err)
// Register second service
_, err = f.msgServer.RegisterService(f.ctx, &types.MsgRegisterService{
Creator: "idx1test",
ServiceId: "service2",
Domain: "test.org",
RequestedPermissions: []string{"register", "update"},
UcanDelegationChain: "",
})
require.NoError(err)
// Query services by owner
resp, err := f.queryServer.ServicesByOwner(f.ctx, &types.QueryServicesByOwnerRequest{
Owner: "idx1test",
})
require.NoError(err)
require.Len(resp.Services, 2)
// Check that both services are returned
serviceIds := make([]string, len(resp.Services))
for i, service := range resp.Services {
serviceIds[i] = service.Id
require.Equal("idx1test", service.Owner)
}
require.Contains(serviceIds, "service1")
require.Contains(serviceIds, "service2")
}
func TestQueryServicesByDomain(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
// Setup verified domain
_, err := f.k.InitiateDomainVerification(f.ctx, "example.com", "idx1test")
require.NoError(err)
err = f.k.SetDomainVerified(f.ctx, "example.com")
require.NoError(err)
// Register service for the domain
_, err = f.msgServer.RegisterService(f.ctx, &types.MsgRegisterService{
Creator: "idx1test",
ServiceId: "domain-service",
Domain: "example.com",
RequestedPermissions: []string{"register"},
UcanDelegationChain: "",
})
require.NoError(err)
// Query services by domain
resp, err := f.queryServer.ServicesByDomain(f.ctx, &types.QueryServicesByDomainRequest{
Domain: "example.com",
})
require.NoError(err)
require.Len(resp.Services, 1)
require.Equal("domain-service", resp.Services[0].Id)
require.Equal("example.com", resp.Services[0].Domain)
}
func TestQueryErrors(t *testing.T) {
f := SetupTest(t)
require := require.New(t)
// Test empty domain
_, err := f.queryServer.DomainVerification(f.ctx, &types.QueryDomainVerificationRequest{
Domain: "",
})
require.Error(err)
require.Contains(err.Error(), "domain cannot be empty")
// Test empty service ID
_, err = f.queryServer.Service(f.ctx, &types.QueryServiceRequest{
ServiceId: "",
})
require.Error(err)
require.Contains(err.Error(), "service_id cannot be empty")
// Test empty owner
_, err = f.queryServer.ServicesByOwner(f.ctx, &types.QueryServicesByOwnerRequest{
Owner: "",
})
require.Error(err)
require.Contains(err.Error(), "owner cannot be empty")
// Test empty domain for services query
_, err = f.queryServer.ServicesByDomain(f.ctx, &types.QueryServicesByDomainRequest{
Domain: "",
})
require.Error(err)
require.Contains(err.Error(), "domain cannot be empty")
// Test non-existent domain
_, err = f.queryServer.DomainVerification(f.ctx, &types.QueryDomainVerificationRequest{
Domain: "nonexistent.com",
})
require.Error(err)
require.Contains(err.Error(), "domain verification not found")
// Test non-existent service
_, err = f.queryServer.Service(f.ctx, &types.QueryServiceRequest{
ServiceId: "nonexistent-service",
})
require.Error(err)
require.Contains(err.Error(), "service not found")
}