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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/coverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ code does not implement. Machine-readable: [`coverage.json`](./coverage.json).
| `cloudtasks` | - | - | [CloudTasks](./gcp/cloudtasks.md) | - | 11 |
| `cloudtrail` | [CloudTrail](./aws/cloudtrail.md) | - | - | - | 60 |
| `codeartifact` | [CodeArtifact](./aws/codeartifact.md) | - | - | - | 15 |
| `cognito` | [Cognito](./aws/cognito.md) | - | - | - | 18 |
| `cognito` | [Cognito](./aws/cognito.md) | - | - | - | 29 |
| `communication` | - | [Communication](./azure/communication.md) | - | - | 10 |
| `composer` | - | - | [Composer](./gcp/composer.md) | - | 6 |
| `compute` | [EC2](./aws/ec2.md) | [VirtualMachines](./azure/virtualmachines.md) | [GCE](./gcp/gce.md) | - | 37 |
Expand Down
2 changes: 1 addition & 1 deletion docs/coverage/aws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Services cloudemu emulates for AWS, by native name. Back to the [cross-provider
| [CloudWatch](./cloudwatch.md) | `monitoring` | 12 |
| [CloudWatchLogs](./cloudwatchlogs.md) | `logging` | 17 |
| [CodeArtifact](./codeartifact.md) | `codeartifact` | 15 |
| [Cognito](./cognito.md) | `cognito` | 18 |
| [Cognito](./cognito.md) | `cognito` | 29 |
| [Config](./config.md) | `configservice` | 102 |
| [CostExplorer](./costexplorer.md) | (provider-native) | 4 |
| [DynamoDB](./dynamodb.md) | `database` | 24 |
Expand Down
15 changes: 13 additions & 2 deletions docs/coverage/aws/cognito.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@

AWS's `cognito` service · portable interface `driver.Cognito` · [AWS index](./README.md)

## Operations (18)
## Operations (29)

| Operation | Description |
| --- | --- |
| `AddCustomAttributes` | AddCustomAttributes appends custom attributes to a pool's schema. Names get |
| `AdminCreateUser` | AdminCreateUser creates a user in FORCE_CHANGE_PASSWORD with a generated |
| `AdminDeleteUser` | |
| `AdminDeleteUserAttributes` | |
| `AdminDisableUser` | |
| `AdminEnableUser` | |
| `AdminGetUser` | |
| `AdminResetUserPassword` | AdminResetUserPassword moves the user to RESET_REQUIRED. |
| `AdminSetUserPassword` | AdminSetUserPassword sets a password checked against the pool policy. A |
| `AdminUpdateUserAttributes` | |
| `CreateUserPool` | CreateUserPool creates a user pool, generating its id and ARN, seeding the |
| `CreateUserPoolClient` | CreateUserPoolClient creates an app client, generating its 26-character id |
| `CreateUserPoolDomain` | |
| `DeleteUserPool` | DeleteUserPool removes a user pool and its clients, domains, and tags. |
| `DeleteUserPool` | DeleteUserPool removes a user pool with its users, clients and tags. Like |
| `DeleteUserPoolClient` | |
| `DeleteUserPoolDomain` | |
| `DescribeUserPool` | DescribeUserPool returns a deep copy of a user pool, or a |
Expand All @@ -20,6 +30,7 @@ AWS's `cognito` service · portable interface `driver.Cognito` · [AWS index](./
| `ListTagsForResource` | |
| `ListUserPoolClients` | ListUserPoolClients returns client descriptions in a user pool in a |
| `ListUserPools` | ListUserPools returns pool descriptions in a deterministic order. |
| `ListUsers` | ListUsers returns users sorted by username, filtered by an optional |
| `SetUserPoolMfaConfig` | SetUserPoolMfaConfig replaces a pool's MFA configuration and returns the |
| `TagResource` | |
| `UntagResource` | |
Expand Down
40 changes: 39 additions & 1 deletion docs/coverage/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -3783,6 +3783,40 @@
"service": "cognito",
"interface": "Cognito",
"operations": [
{
"name": "AddCustomAttributes",
"doc": "AddCustomAttributes appends custom attributes to a pool's schema. Names get"
},
{
"name": "AdminCreateUser",
"doc": "AdminCreateUser creates a user in FORCE_CHANGE_PASSWORD with a generated"
},
{
"name": "AdminDeleteUser"
},
{
"name": "AdminDeleteUserAttributes"
},
{
"name": "AdminDisableUser"
},
{
"name": "AdminEnableUser"
},
{
"name": "AdminGetUser"
},
{
"name": "AdminResetUserPassword",
"doc": "AdminResetUserPassword moves the user to RESET_REQUIRED."
},
{
"name": "AdminSetUserPassword",
"doc": "AdminSetUserPassword sets a password checked against the pool policy. A"
},
{
"name": "AdminUpdateUserAttributes"
},
{
"name": "CreateUserPool",
"doc": "CreateUserPool creates a user pool, generating its id and ARN, seeding the"
Expand All @@ -3796,7 +3830,7 @@
},
{
"name": "DeleteUserPool",
"doc": "DeleteUserPool removes a user pool and its clients, domains, and tags."
"doc": "DeleteUserPool removes a user pool with its users, clients and tags. Like"
},
{
"name": "DeleteUserPoolClient"
Expand Down Expand Up @@ -3830,6 +3864,10 @@
"name": "ListUserPools",
"doc": "ListUserPools returns pool descriptions in a deterministic order."
},
{
"name": "ListUsers",
"doc": "ListUsers returns users sorted by username, filtered by an optional"
},
{
"name": "SetUserPoolMfaConfig",
"doc": "SetUserPoolMfaConfig replaces a pool's MFA configuration and returns the"
Expand Down
19 changes: 9 additions & 10 deletions providers/aws/cognito/cognito.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
// Package cognito provides an in-memory mock implementation of the AWS Cognito
// user-pools (cognito-idp) control plane: user pools, their app clients, and
// hosted-UI domains, plus resource tagging.
// Package cognito provides an in-memory mock of AWS Cognito user pools
// (cognito-idp): user pools, their app clients, hosted-UI domains, resource
// tagging, and pool users with the admin user-management operations.
//
// This is the configuration control plane only. There is no authentication data
// plane behind the emulator (sign-up, sign-in, token issuance, users, and
// groups are out of scope), so the mock covers provisioning and reading pools,
// clients, and domains and their settings, which is what IaC tools (Terraform,
// CloudFormation) and the console's create flow exercise.
// Sign-up, sign-in and token issuance are not modeled yet.
package cognito

import (
Expand All @@ -29,13 +25,15 @@ const clientKeySep = "/"
// plane.
type Mock struct {
// userPools is keyed by pool id; clients is keyed by "<poolID>/<clientID>";
// domains is keyed by the domain string.
// domains is keyed by the domain string; users is keyed by
// "<poolID>/<username>".
userPools *memstore.Store[driver.UserPool]
clients *memstore.Store[driver.UserPoolClient]
domains *memstore.Store[driver.UserPoolDomain]
users *memstore.Store[userRecord]

// mu serializes compound read-modify-write mutations (pool update, cascading
// pool delete) that span more than one store operation.
// pool delete, user changes) that span more than one store operation.
mu sync.Mutex

// tagsMu guards the resource-tag side map, keyed by resource ARN.
Expand All @@ -51,6 +49,7 @@ func New(opts *config.Options) *Mock {
userPools: memstore.New[driver.UserPool](),
clients: memstore.New[driver.UserPoolClient](),
domains: memstore.New[driver.UserPoolDomain](),
users: memstore.New[userRecord](),
tags: map[string]map[string]string{},
opts: opts,
}
Expand Down
30 changes: 3 additions & 27 deletions providers/aws/cognito/cognito_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ func TestUpdateAndDeleteUserPool(t *testing.T) {
t.Fatalf("update not applied: %+v", got)
}

requireNoError(t, m.DeleteUserPool(context.Background(), pool.ID), "DeleteUserPool")
assertNotFound(t, m.DeleteUserPool(context.Background(), pool.ID))
if err := m.DeleteUserPool(context.Background(), pool.ID); err == nil {
t.Fatal("DeleteUserPool succeeded with deletion protection ACTIVE")
}
}

func TestClientSecretOnlyWithGenerate(t *testing.T) {
Expand Down Expand Up @@ -282,31 +283,6 @@ func TestUserPoolDomainLifecycle(t *testing.T) {
}
}

func TestDeletePoolCascades(t *testing.T) {
m := newMock(t)
pool := mustCreatePool(t, m, "cascade-pool")
ctx := context.Background()

client, err := m.CreateUserPoolClient(ctx, driver.CreateUserPoolClientInput{UserPoolID: pool.ID, ClientName: "c"})
requireNoError(t, err, "CreateUserPoolClient")

requireNoError(t, m.CreateUserPoolDomain(ctx,
driver.CreateUserPoolDomainInput{Domain: "d.example", UserPoolID: pool.ID}), "CreateUserPoolDomain")

requireNoError(t, m.DeleteUserPool(ctx, pool.ID), "DeleteUserPool")

if _, err := m.DescribeUserPoolClient(ctx, pool.ID, client.ClientID); !cerrors.IsNotFound(err) {
t.Fatal("client not removed on pool delete")
}

dom, err := m.DescribeUserPoolDomain(ctx, "d.example")
requireNoError(t, err, "DescribeUserPoolDomain")

if dom.Domain != "" {
t.Fatal("domain not removed on pool delete")
}
}

func TestTagsRoundTripAndReplace(t *testing.T) {
m := newMock(t)
ctx := context.Background()
Expand Down
88 changes: 88 additions & 0 deletions providers/aws/cognito/custom_attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package cognito

import (
"context"
"strings"

"github.com/stackshy/cloudemu/v2/services/cognito/driver"
)

// Custom attribute limits: at most 50 per pool, and a name (without its
// custom: prefix) of 1 to 20 characters.
const (
maxCustomAttributes = 50
maxCustomNameLen = 20
)

// AddCustomAttributes appends custom attributes to a pool's schema. Each name
// gets the custom: prefix (dev: for developer-only). Cognito never changes or
// removes an attribute once added, so a name already in the schema, or repeated
// in the request, is rejected and nothing is added. A name may be given with or
// without its custom: prefix.
func (m *Mock) AddCustomAttributes(_ context.Context, userPoolID string, attrs []driver.SchemaAttribute) error {
if len(attrs) == 0 {
return invalidParameter("1 validation error detected: Value null at 'customAttributes' failed to satisfy constraint: " +
"Member must not be null")
}

m.mu.Lock()
defer m.mu.Unlock()

pool, ok := m.userPools.Get(userPoolID)
if !ok {
return poolNotFound(userPoolID)
}

pool = copyUserPool(pool)
schema := pool.SchemaAttributes

for i, a := range attrs {
if n := len(bareCustomName(a.Name)); n < 1 || n > maxCustomNameLen {
return invalidParameter("1 validation error detected: Value '%s' at 'customAttributes.%d.member.name' "+
"failed to satisfy constraint: Member must have length between 1 and %d", a.Name, i+1, maxCustomNameLen)
}

if a.AttributeDataType == "" {
a.AttributeDataType = driver.AttributeTypeString
}

added := customAttribute(a)
if _, exists := schemaAttributeIn(schema, added.Name); exists {
return invalidParameter("Existing attribute already has name %s.", added.Name)
}

schema = append(schema, added)
}

if countCustom(schema) > maxCustomAttributes {
return invalidParameter("The user pool has reached the limit of %d custom attributes.", maxCustomAttributes)
}

pool.SchemaAttributes = schema
pool.LastModifiedDate = m.now()
m.userPools.Set(userPoolID, pool)

return nil
}

func schemaAttributeIn(schema []driver.SchemaAttribute, name string) (driver.SchemaAttribute, bool) {
for _, a := range schema {
if a.Name == name {
return a, true
}
}

return driver.SchemaAttribute{}, false
}

func countCustom(schema []driver.SchemaAttribute) int {
n := 0

for _, a := range schema {
if strings.HasPrefix(a.Name, "custom:") || strings.HasPrefix(a.Name, "dev:") {
n++
}
}

return n
}
46 changes: 46 additions & 0 deletions providers/aws/cognito/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,49 @@ func invalidParameter(format string, args ...any) error {
func resourceNotFound(format string, args ...any) error {
return &driver.APIError{Exception: driver.ExResourceNotFound, Err: errors.Newf(errors.NotFound, format, args...)}
}

// poolNotFound is the ResourceNotFoundException for a missing user pool.
func poolNotFound(id string) error {
return resourceNotFound("User pool %s does not exist.", id)
}

// userNotFound is the UserNotFoundException real Cognito returns for an unknown
// username.
func userNotFound() error {
//nolint:revive // exact Cognito message, surfaced verbatim to the SDK
return &driver.APIError{Exception: driver.ExUserNotFound, Err: errors.New(errors.NotFound, "User does not exist.")}
}

// usernameExists builds a UsernameExistsException for a duplicate user.
func usernameExists(msg string) error {
return &driver.APIError{Exception: driver.ExUsernameExists, Err: errors.New(errors.AlreadyExists, msg)}
}

// aliasExists builds the AliasExistsException for a sign-in value (email, phone
// number or preferred username) that another user already holds.
func aliasExists(attr string) error {
return &driver.APIError{
Exception: driver.ExAliasExists,
Err: errors.New(errors.AlreadyExists, "An account with the given "+attr+" already exists."),
}
}

// invalidPassword builds an InvalidPasswordException for a password that breaks
// the pool's policy.
func invalidPassword(reason string) error {
return &driver.APIError{
Exception: driver.ExInvalidPassword,
Err: errors.New(errors.InvalidArgument, "Password did not conform with policy: "+reason),
}
}

// notAuthorized builds a NotAuthorizedException for an operation the user's
// current state does not allow.
func notAuthorized(msg string) error {
return &driver.APIError{Exception: driver.ExNotAuthorized, Err: errors.New(errors.FailedPrecondition, msg)}
}

// unsupportedUserState builds an UnsupportedUserStateException.
func unsupportedUserState(format string, args ...any) error {
return &driver.APIError{Exception: driver.ExUnsupportedUserState, Err: errors.Newf(errors.FailedPrecondition, format, args...)}
}
Loading
Loading