package command import ( "context" "strings" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) // InstanceOrgSetup is used for the first organisation in the instance setup. // It used to be called OrgSetup, which now allows multiple Users, but it's used in the config.yaml and therefore // a breaking change was not possible. type InstanceOrgSetup struct { Name string CustomDomain string Human *AddHuman Machine *AddMachine Roles []string } type OrgSetup struct { Name string CustomDomain string Admins []*OrgSetupAdmin } // OrgSetupAdmin describes a user to be created (Human / Machine) or an existing (ID) to be used for an org setup. type OrgSetupAdmin struct { ID string Human *AddHuman Machine *AddMachine Roles []string } type orgSetupCommands struct { validations []preparation.Validation aggregate *org.Aggregate commands *Commands admins []*OrgSetupAdmin pats []*PersonalAccessToken machineKeys []*MachineKey } type CreatedOrg struct { ObjectDetails *domain.ObjectDetails CreatedAdmins []*CreatedOrgAdmin } type CreatedOrgAdmin struct { ID string EmailCode *string PhoneCode *string PAT *PersonalAccessToken MachineKey *MachineKey } func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, allowInitialMail bool, userIDs ...string) (_ *CreatedOrg, err error) { cmds := c.newOrgSetupCommands(ctx, orgID, o, userIDs) for _, admin := range o.Admins { if err = cmds.setupOrgAdmin(admin, allowInitialMail); err != nil { return nil, err } } if err = cmds.addCustomDomain(o.CustomDomain, userIDs); err != nil { return nil, err } return cmds.push(ctx) } func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSetup *OrgSetup, userIDs []string) *orgSetupCommands { orgAgg := org.NewAggregate(orgID) validations := []preparation.Validation{ AddOrgCommand(ctx, orgAgg, orgSetup.Name, userIDs...), } return &orgSetupCommands{ validations: validations, aggregate: orgAgg, commands: c, admins: orgSetup.Admins, } } func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail bool) error { if admin.ID != "" { c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, admin.ID, orgAdminRoles(admin.Roles)...)) return nil } userID, err := c.commands.idGenerator.Next() if err != nil { return err } if admin.Human != nil { admin.Human.ID = userID c.validations = append(c.validations, c.commands.AddHumanCommand(admin.Human, c.aggregate.ID, c.commands.userPasswordHasher, c.commands.userEncryption, allowInitialMail)) } else if admin.Machine != nil { admin.Machine.Machine.AggregateID = userID if err = c.setupOrgAdminMachine(c.aggregate, admin.Machine); err != nil { return err } } c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, userID, orgAdminRoles(admin.Roles)...)) return nil } func (c *orgSetupCommands) setupOrgAdminMachine(orgAgg *org.Aggregate, machine *AddMachine) error { userAgg := user.NewAggregate(machine.Machine.AggregateID, orgAgg.ID) c.validations = append(c.validations, AddMachineCommand(userAgg, machine.Machine)) var pat *PersonalAccessToken var machineKey *MachineKey if machine.Pat != nil { pat = NewPersonalAccessToken(orgAgg.ID, machine.Machine.AggregateID, machine.Pat.ExpirationDate, machine.Pat.Scopes, domain.UserTypeMachine) tokenID, err := c.commands.idGenerator.Next() if err != nil { return err } pat.TokenID = tokenID c.pats = append(c.pats, pat) c.validations = append(c.validations, prepareAddPersonalAccessToken(pat, c.commands.keyAlgorithm)) } if machine.MachineKey != nil { machineKey = NewMachineKey(orgAgg.ID, machine.Machine.AggregateID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type) keyID, err := c.commands.idGenerator.Next() if err != nil { return err } machineKey.KeyID = keyID c.machineKeys = append(c.machineKeys, machineKey) c.validations = append(c.validations, prepareAddUserMachineKey(machineKey, c.commands.keySize)) } return nil } func (c *orgSetupCommands) addCustomDomain(domain string, userIDs []string) error { if domain != "" { c.validations = append(c.validations, c.commands.prepareAddOrgDomain(c.aggregate, domain, userIDs)) } return nil } func orgAdminRoles(roles []string) []string { if len(roles) > 0 { return roles } return []string{domain.RoleOrgOwner} } func (c *orgSetupCommands) push(ctx context.Context) (_ *CreatedOrg, err error) { cmds, err := preparation.PrepareCommands(ctx, c.commands.eventstore.Filter, c.validations...) if err != nil { return nil, err } events, err := c.commands.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } return &CreatedOrg{ ObjectDetails: &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreatedAt(), ResourceOwner: c.aggregate.ID, }, CreatedAdmins: c.createdAdmins(), }, nil } func (c *orgSetupCommands) createdAdmins() []*CreatedOrgAdmin { users := make([]*CreatedOrgAdmin, 0, len(c.admins)) for _, admin := range c.admins { if admin.ID != "" { continue } if admin.Human != nil { users = append(users, c.createdHumanAdmin(admin)) continue } if admin.Machine != nil { users = append(users, c.createdMachineAdmin(admin)) } } return users } func (c *orgSetupCommands) createdHumanAdmin(admin *OrgSetupAdmin) *CreatedOrgAdmin { createdAdmin := &CreatedOrgAdmin{ ID: admin.Human.ID, } if admin.Human.EmailCode != nil { createdAdmin.EmailCode = admin.Human.EmailCode } return createdAdmin } func (c *orgSetupCommands) createdMachineAdmin(admin *OrgSetupAdmin) *CreatedOrgAdmin { createdAdmin := &CreatedOrgAdmin{ ID: admin.Machine.Machine.AggregateID, } if admin.Machine.Pat != nil { for _, pat := range c.pats { if pat.AggregateID == createdAdmin.ID { createdAdmin.PAT = pat } } } if admin.Machine.MachineKey != nil { for _, key := range c.machineKeys { if key.AggregateID == createdAdmin.ID { createdAdmin.MachineKey = key } } } return createdAdmin } func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail bool, userIDs ...string) (*CreatedOrg, error) { orgID, err := c.idGenerator.Next() if err != nil { return nil, err } return c.setUpOrgWithIDs(ctx, o, orgID, allowInitialMail, userIDs...) } // AddOrgCommand defines the commands to create a new org, // this includes the verified default domain func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string, userIDs ...string) preparation.Validation { return func() (preparation.CreateCommands, error) { if name = strings.TrimSpace(name); name == "" { return nil, zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument") } defaultDomain, err := domain.NewIAMDomainName(name, authz.GetInstance(ctx).RequestedDomain()) if err != nil { return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return []eventstore.Command{ org.NewOrgAddedEvent(ctx, &a.Aggregate, name), org.NewDomainAddedEvent(ctx, &a.Aggregate, defaultDomain), org.NewDomainVerifiedEvent(ctx, &a.Aggregate, defaultDomain), org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, defaultDomain), }, nil }, nil } } func (c *Commands) getOrg(ctx context.Context, orgID string) (*domain.Org, error) { writeModel, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { return nil, err } if !isOrgStateExists(writeModel.State) { return nil, zerrors.ThrowInternal(err, "COMMAND-4M9sf", "Errors.Org.NotFound") } return orgWriteModelToOrg(writeModel), nil } func (c *Commands) checkOrgExists(ctx context.Context, orgID string) error { orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { return err } if !isOrgStateExists(orgWriteModel.State) { return zerrors.ThrowPreconditionFailed(nil, "COMMAND-QXPGs", "Errors.Org.NotFound") } return nil } func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (*domain.Org, error) { existingOrg, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { return nil, err } if existingOrg.State != domain.OrgStateUnspecified { return nil, zerrors.ThrowNotFound(nil, "ORG-lapo2m", "Errors.Org.AlreadyExisting") } return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) } func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner string, claimedUserIDs []string) (*domain.Org, error) { if name = strings.TrimSpace(name); name == "" { return nil, zerrors.ThrowInvalidArgument(nil, "EVENT-Mf9sd", "Errors.Org.Invalid") } orgID, err := c.idGenerator.Next() if err != nil { return nil, zerrors.ThrowInternal(err, "COMMA-OwciI", "Errors.Internal") } return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) } func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (*domain.Org, error) { orgAgg, addedOrg, events, err := c.addOrgWithID(ctx, &domain.Org{Name: name}, orgID, claimedUserIDs) if err != nil { return nil, err } err = c.checkUserExists(ctx, userID, resourceOwner) if err != nil { return nil, err } addedMember := NewOrgMemberWriteModel(addedOrg.AggregateID, userID) orgMemberEvent, err := c.addOrgMember(ctx, orgAgg, addedMember, domain.NewMember(orgAgg.ID, userID, domain.RoleOrgOwner)) if err != nil { return nil, err } events = append(events, orgMemberEvent) pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, err } err = AppendAndReduce(addedOrg, pushedEvents...) if err != nil { return nil, err } return orgWriteModelToOrg(addedOrg), nil } func (c *Commands) ChangeOrg(ctx context.Context, orgID, name string) (*domain.ObjectDetails, error) { name = strings.TrimSpace(name) if orgID == "" || name == "" { return nil, zerrors.ThrowInvalidArgument(nil, "EVENT-Mf9sd", "Errors.Org.Invalid") } orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { return nil, err } if !isOrgStateExists(orgWriteModel.State) { return nil, zerrors.ThrowNotFound(nil, "ORG-1MRds", "Errors.Org.NotFound") } if orgWriteModel.Name == name { return nil, zerrors.ThrowPreconditionFailed(nil, "ORG-4VSdf", "Errors.Org.NotChanged") } orgAgg := OrgAggregateFromWriteModel(&orgWriteModel.WriteModel) events := make([]eventstore.Command, 0) events = append(events, org.NewOrgChangedEvent(ctx, orgAgg, orgWriteModel.Name, name)) changeDomainEvents, err := c.changeDefaultDomain(ctx, orgID, name) if err != nil { return nil, err } if len(changeDomainEvents) > 0 { events = append(events, changeDomainEvents...) } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, err } err = AppendAndReduce(orgWriteModel, pushedEvents...) if err != nil { return nil, err } return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil } func (c *Commands) DeactivateOrg(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { return nil, err } if !isOrgStateExists(orgWriteModel.State) { return nil, zerrors.ThrowNotFound(nil, "ORG-oL9nT", "Errors.Org.NotFound") } if orgWriteModel.State == domain.OrgStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "EVENT-Dbs2g", "Errors.Org.AlreadyDeactivated") } orgAgg := OrgAggregateFromWriteModel(&orgWriteModel.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, org.NewOrgDeactivatedEvent(ctx, orgAgg)) if err != nil { return nil, err } err = AppendAndReduce(orgWriteModel, pushedEvents...) if err != nil { return nil, err } return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil } func (c *Commands) ReactivateOrg(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { return nil, err } if !isOrgStateExists(orgWriteModel.State) { return nil, zerrors.ThrowNotFound(nil, "ORG-Dgf3g", "Errors.Org.NotFound") } if orgWriteModel.State == domain.OrgStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "EVENT-bfnrh", "Errors.Org.AlreadyActive") } orgAgg := OrgAggregateFromWriteModel(&orgWriteModel.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, org.NewOrgReactivatedEvent(ctx, orgAgg)) if err != nil { return nil, err } err = AppendAndReduce(orgWriteModel, pushedEvents...) if err != nil { return nil, err } return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil } func (c *Commands) RemoveOrg(ctx context.Context, id string) (*domain.ObjectDetails, error) { orgAgg := org.NewAggregate(id) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareRemoveOrg(orgAgg)) if err != nil { return nil, err } events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } return &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreatedAt(), ResourceOwner: events[len(events)-1].Aggregate().InstanceID, }, nil } func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation { return func() (preparation.CreateCommands, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { instance := authz.GetInstance(ctx) if a.ID == instance.DefaultOrganisationID() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-wG9p1", "Errors.Org.DefaultOrgNotDeletable") } err := c.checkProjectExists(ctx, instance.ProjectID(), a.ID) // if there is no error, the ZITADEL project was found on the org to be deleted if err == nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMA-AF3JW", "Errors.Org.ZitadelOrgNotDeletable") } // "precondition failed" error means the project does not exist, return other errors if !zerrors.IsPreconditionFailed(err) { return nil, err } writeModel, err := c.getOrgWriteModelByID(ctx, a.ID) if err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMA-wG9p1", "Errors.Org.NotFound") } if !isOrgStateExists(writeModel.State) { return nil, zerrors.ThrowNotFound(nil, "COMMA-aps2n", "Errors.Org.NotFound") } domainPolicy, err := c.domainPolicyWriteModel(ctx, a.ID) if err != nil { return nil, err } usernames, err := OrgUsers(ctx, filter, a.ID) if err != nil { return nil, err } domains, err := OrgDomains(ctx, filter, a.ID) if err != nil { return nil, err } links, err := OrgUserIDPLinks(ctx, filter, a.ID) if err != nil { return nil, err } entityIds, err := OrgSamlEntityIDs(ctx, filter, a.ID) if err != nil { return nil, err } return []eventstore.Command{org.NewOrgRemovedEvent(ctx, &a.Aggregate, writeModel.Name, usernames, domainPolicy.UserLoginMustBeDomain, domains, links, entityIds)}, nil }, nil } } func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReducer, orgID string) ([]*domain.UserIDPLink, error) { events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(orgID). OrderAsc(). AddQuery(). AggregateTypes(user.AggregateType). EventTypes( user.UserIDPLinkAddedType, user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType, ).Builder()) if err != nil { return nil, err } links := make([]*domain.UserIDPLink, 0) for _, event := range events { switch eventTyped := event.(type) { case *user.UserIDPLinkAddedEvent: links = append(links, &domain.UserIDPLink{ IDPConfigID: eventTyped.IDPConfigID, ExternalUserID: eventTyped.ExternalUserID, DisplayName: eventTyped.DisplayName, }) case *user.UserIDPLinkRemovedEvent: for i := range links { if links[i].ExternalUserID == eventTyped.ExternalUserID && links[i].IDPConfigID == eventTyped.IDPConfigID { links[i] = links[len(links)-1] links[len(links)-1] = nil links = links[:len(links)-1] break } } case *user.UserIDPLinkCascadeRemovedEvent: for i := range links { if links[i].ExternalUserID == eventTyped.ExternalUserID && links[i].IDPConfigID == eventTyped.IDPConfigID { links[i] = links[len(links)-1] links[len(links)-1] = nil links = links[:len(links)-1] break } } } } return links, nil } type samlEntityID struct { appID string entityID string } func OrgSamlEntityIDs(ctx context.Context, filter preparation.FilterToQueryReducer, orgID string) ([]string, error) { events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(orgID). OrderAsc(). AddQuery(). AggregateTypes(project.AggregateType). EventTypes( project.SAMLConfigAddedType, project.SAMLConfigChangedType, project.ApplicationRemovedType, ).Builder()) if err != nil { return nil, err } entityIDs := make([]samlEntityID, 0) for _, event := range events { switch eventTyped := event.(type) { case *project.SAMLConfigAddedEvent: entityIDs = append(entityIDs, samlEntityID{appID: eventTyped.AppID, entityID: eventTyped.EntityID}) case *project.SAMLConfigChangedEvent: for i := range entityIDs { if entityIDs[i].appID == eventTyped.AppID { entityIDs[i].entityID = eventTyped.EntityID break } } case *project.ApplicationRemovedEvent: for i := range entityIDs { if entityIDs[i].appID == eventTyped.AppID { entityIDs[i] = entityIDs[len(entityIDs)-1] entityIDs = entityIDs[:len(entityIDs)-1] break } } } } ids := make([]string, len(entityIDs)) for i := range entityIDs { ids[i] = entityIDs[i].entityID } return ids, nil } func OrgDomains(ctx context.Context, filter preparation.FilterToQueryReducer, orgID string) ([]string, error) { events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(orgID). OrderAsc(). AddQuery(). AggregateTypes(org.AggregateType). EventTypes( org.OrgDomainVerifiedEventType, org.OrgDomainRemovedEventType, ).Builder()) if err != nil { return nil, err } names := make([]string, 0) for _, event := range events { switch eventTyped := event.(type) { case *org.DomainVerifiedEvent: names = append(names, eventTyped.Domain) case *org.DomainRemovedEvent: for i := range names { if names[i] == eventTyped.Domain { names[i] = names[len(names)-1] names = names[:len(names)-1] break } } } } return names, nil } type userIDName struct { name string id string } func OrgUsers(ctx context.Context, filter preparation.FilterToQueryReducer, orgID string) ([]string, error) { events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). InstanceID(authz.GetInstance(ctx).InstanceID()). ResourceOwner(orgID). OrderAsc(). AddQuery(). AggregateTypes(user.AggregateType). EventTypes( user.HumanAddedType, user.MachineAddedEventType, user.HumanRegisteredType, user.UserDomainClaimedType, user.UserUserNameChangedType, user.UserRemovedType, ).Builder()) if err != nil { return nil, err } users := make([]userIDName, 0) for _, event := range events { switch eventTyped := event.(type) { case *user.HumanAddedEvent: users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID}) case *user.MachineAddedEvent: users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID}) case *user.HumanRegisteredEvent: users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID}) case *user.DomainClaimedEvent: for i := range users { if users[i].id == eventTyped.Aggregate().ID { users[i].name = eventTyped.UserName } } case *user.UsernameChangedEvent: for i := range users { if users[i].id == eventTyped.Aggregate().ID { users[i].name = eventTyped.UserName } } case *user.UserRemovedEvent: for i := range users { if users[i].id == eventTyped.Aggregate().ID { users[i] = users[len(users)-1] users = users[:len(users)-1] break } } } } names := make([]string, len(users)) for i := range users { names[i] = users[i].name } return names, nil } func ExistsOrg(ctx context.Context, filter preparation.FilterToQueryReducer, id string) (exists bool, err error) { events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). InstanceID(authz.GetInstance(ctx).InstanceID()). ResourceOwner(id). OrderAsc(). AddQuery(). AggregateTypes(org.AggregateType). AggregateIDs(id). EventTypes( org.OrgAddedEventType, org.OrgDeactivatedEventType, org.OrgReactivatedEventType, org.OrgRemovedEventType, ).Builder()) if err != nil { return false, err } for _, event := range events { switch event.(type) { case *org.OrgAddedEvent, *org.OrgReactivatedEvent: exists = true case *org.OrgDeactivatedEvent, *org.OrgRemovedEvent: exists = false } } return exists, nil } func (c *Commands) addOrgWithID(ctx context.Context, organisation *domain.Org, orgID string, claimedUserIDs []string) (_ *eventstore.Aggregate, _ *OrgWriteModel, _ []eventstore.Command, err error) { if !organisation.IsValid() { return nil, nil, nil, zerrors.ThrowInvalidArgument(nil, "COMM-deLSk", "Errors.Org.Invalid") } organisation.AggregateID = orgID organisation.AddIAMDomain(authz.GetInstance(ctx).RequestedDomain()) addedOrg := NewOrgWriteModel(organisation.AggregateID) orgAgg := OrgAggregateFromWriteModel(&addedOrg.WriteModel) events := []eventstore.Command{ org.NewOrgAddedEvent(ctx, orgAgg, organisation.Name), } for _, orgDomain := range organisation.Domains { orgDomainEvents, err := c.addOrgDomain(ctx, orgAgg, NewOrgDomainWriteModel(orgAgg.ID, orgDomain.Domain), orgDomain, claimedUserIDs) if err != nil { return nil, nil, nil, err } events = append(events, orgDomainEvents...) } return orgAgg, addedOrg, events, nil } func (c *Commands) getOrgWriteModelByID(ctx context.Context, orgID string) (*OrgWriteModel, error) { orgWriteModel := NewOrgWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, orgWriteModel) if err != nil { return nil, err } return orgWriteModel, nil } func isOrgStateExists(state domain.OrgState) bool { return !hasOrgState(state, domain.OrgStateRemoved, domain.OrgStateUnspecified) } func hasOrgState(check domain.OrgState, states ...domain.OrgState) bool { for _, state := range states { if check == state { return true } } return false }