!=========================================================================
! Copyright (C) VMware, Inc. 1986-2011.  All Rights Reserved.
!
! $Id: userprofileset.gs,v 1.18 2008-01-09 22:50:20 stever Exp $
!
! Superclass Hierarchy:
!   UserProfileSet, AbstractUserProfileSet, IdentitySet, IdentityBag,
!   UnorderedCollection, Collection, Object.
!
!=========================================================================

removeallmethods UserProfileSet
removeallclassmethods UserProfileSet

category: 'For Documentation Installation only'
classmethod: UserProfileSet
installDocumentation

| doc txt |
doc := GsClassDocumentation newForClass: self.

txt := (GsDocText new) details:
'UserProfileSet is a concrete subclass of AbstractUserProfileSet that implements
 and enforces some account and password security features for all of its
 elements.

 Only one instance of UserProfileSet, called AllUsers, is allowed to exist,
 and is provided with a fresh GemStone server.  All UserProfiles in GemStone 
 belong to this set.  AllUsers supports security features for all users.  
 AllUsers also ensures uniqueness of userId Strings; no two UserProfiles can
 have the same ID.

 GemStone, as shipped from the factory, disables all the security features
 supported by AllUsers.  To activate any or all of those features, an
 administrator with the proper privileges must execute methods on AllUsers.
 Activated features can also be deactivated later by reapplying the settings
 that do not constrain GemStone''s behavior.

 Password format constraints are applied only after an administrator commits
 the changes in AllUsers.  They are then enforced only when users change their
 own passwords with the UserProfile>>oldPassword:newPassword: method, not when
 administrators or other users make changes with methods that require the
 OtherPassword privilege.  In addition, enforcement does not apply to existing
 passwords created before new constraints were committed.
' .
doc documentClassWith: txt.

txt := (GsDocText new) details:
'A StringKeyValueDictionary whose keys are userId Strings and whose values are
 the UserProfiles of the instance.' .
doc documentInstVar: #userIdDictionary with: txt.

txt := (GsDocText new) details:
'The userSecurityData variable is used internally by GemStone.' .
doc documentInstVar: #userSecurityData with: txt.

txt := (GsDocText new) details:
'The maximum Number of hours for which a user can retain a password.  When a
 password is set, it expires this Number of hours later.  The user must change
 the password before it expires, or else GemStone disables the account.  Once
 a password has expired, an administrator must reset the password from another
 account before the user can login again.

 Zero means there is no expiration time for passwords.' .
doc documentInstVar: #passwordAgeLimit with: txt.

txt := (GsDocText new) details:
'The maximum Number of hours prior to a password expiration time for which a
 user can login without a warning.  If the user logs in to GemStone within this
 Number of hours before the password is due to expire, GemStone issues a warning
 about the impending expiration.  This feature grants a user the opportunity to
 change the password conveniently and to prevent the account from being
 disabled.' .
doc documentInstVar: #passwordAgeWarning with: txt.

txt := (GsDocText new) details:
'The maximum Number of hours for which a user account can remain enabled without
 a login.  Once the user logs in, he or she has up to this Number of hours to
 login again, or else GemStone disables the account.  Once the account has been
 disabled, an administrator must reset the password from another account before
 the user can login again.

 Zero means there is no expiration time for accounts.' .
doc documentInstVar: #staleAccountAgeLimit with: txt.

txt := (GsDocText new) details:
'A Boolean.  When set to true, GemStone does not permit a user to reuse any
 former password from that time forward.  When set to false, GemStone permits
 users to reuse passwords as they wish.' .
doc documentInstVar: #disallowUsedPasswords with: txt.

txt := (GsDocText new) details:
'A Set of Strings that cannot be used as passwords.  The userId Strings of
 GemStone users also cannot be used as passwords, even if they do not appear in
 this Set.' .
doc documentInstVar: #disallowedPasswords with: txt.

txt := (GsDocText new) details:
'A SmallInteger that gives maximum size of new passwords.  Zero means there is
 no maximum.' .
doc documentInstVar: #maxPasswordSize with: txt.

txt := (GsDocText new) details:
'A SmallInteger that gives minimum size of new passwords.  Zero means there is
 no minimum.' .
doc documentInstVar: #minPasswordSize with: txt.

txt := (GsDocText new) details:
'A SmallInteger that gives maximum number of adjacent characters in a password
 that can have the same value.  Zero means there is no maximum.  The value 1
 prevents passwords of the form aa, but not aba.' .
doc documentInstVar: #maxRepeatingChars with: txt.

txt := (GsDocText new) details:
'A SmallInteger that gives maximum number of adjacent characters in a password
 that form an ascending or descending sequence of Character values, such as
 "123" or "fed".  Zero means there is no maximum.  Such sequences are determined
 by case-sensitive comparisons.' .
doc documentInstVar: #maxConsecutiveChars with: txt.

txt := (GsDocText new) details:
'A SmallInteger that gives maximum number of adjacent characters in a password
 that are permitted to have the same type (alphabetic, numeric, or special).
 Zero means there is no maximum.' .
doc documentInstVar: #maxCharsOfSameType with: txt.

self description: doc.
%

! new disallowed , to fix 31257
category: 'Creation'
classmethod: UserProfileSet
new

"Disallowed"

self shouldNotImplement: #new
%

category: 'Accessing'
method: UserProfileSet
membersOfGroup: aString

"Returns an IdentitySet containing the userId for each member of the specified
 group.  If the group contains no members, returns an empty IdentitySet.

 Generates an error if aString is not a kind of String, or if aString is not
 defined in the global object AllGroups."

| memberSet aUserPro theGroup |

memberSet := IdentitySet new.
theGroup := AllGroups _validateGroupString: aString .

"We have a valid group symbol, so build a set of members"

1 to: self size do:[:j|
   aUserPro := self _at: j .
   (aUserPro groups includesIdentical: theGroup)
                   ifTrue:[ memberSet add: (aUserPro userId) ]
   ].
^memberSet
%

category: 'Accessing'
method: UserProfileSet
userWithId: aString ifAbsent: aBlock

"Searches the receiver for a UserProfile whose userId is equal to aString, and
 returns that UserProfile.  Evaluates the argument aBlock if no userId
 is equal to aString."

"Example: (AllUsers userWithId: 'DataCurator') "

| aUserProfile aStr |
aStr := aString asString .
userIdDictionary ~~ nil ifTrue:[
  ^ userIdDictionary at: aStr ifAbsent: aBlock
  ].
aUserProfile:=
   self detect: [:aUserPro | aUserPro.userId = aStr] ifNone: aBlock .

^aUserProfile "Should only be one as UserId are unique"
%

category: 'Formatting'
method: UserProfileSet
dictionaryNames

"Returns a formatted String describing each user's symbol list.  For each user,
 the String contains the userId, and the position and name of each Dictionary
 in that user's symbol list.

 This method assumes that each Dictionary in the symbol list contains an
 Association whose value is that Dictionary.  If any Dictionary does not
 contain such an Association, it is represented in the result String as
 '(unnamed Dictionary)'."

"Example use: 'AllUsers dictionaryNames' "

| tempStr aUserPro |

tempStr := String new.

1 to: self size do:[:j|
  aUserPro := self _at: j .
  tempStr add: (Character lf).
  tempStr add: (aUserPro userId asString).
  tempStr add: (Character lf).
  tempStr add: (aUserPro dictionaryNames).
  ].

^tempStr
%
category: 'Adding'
method: UserProfileSet
add: aUserProfile

"Adds a new UserProfile to the receiver.  Generates an error if aUserProfile
 has a userId that duplicates an existing element of the receiver.
 Requires OtherPassword privilege."

<primitive: 901>
| result |
self _validatePrivilege.
result := self _add: aUserProfile .
System _disableProtectedMode.
^ result
%

category: 'Private'
method: UserProfileSet
__add: aUserProfile

""
"Reimplementation from IdentityBag.
 Adds anObject to the receiver only if it is not already an element of the
 receiver.  Returns anObject.  Has no effect if anObject is nil."

<protected primitive: 208>
self _primitiveFailed: #add: .
self _uncontinuableError
%

category: 'Private'
method: UserProfileSet
_add: aUserProfile

"Adds a new UserProfile to the receiver.  Generates an error if aUserProfile
 has a userId that duplicates an existing element of the receiver."

<protected>

| userId |

aUserProfile _validateClass: UserProfile.
userId := aUserProfile userId.
self userWithId: userId ifAbsent:[
  userIdDictionary ~~ nil ifTrue:[
     userIdDictionary at: userId put: aUserProfile .
     ].
  aUserProfile _newSecurityData .
  self __add: aUserProfile .
  System _disableProtectedMode  "exit protected mode".
  ^ aUserProfile
  ].
System _disableProtectedMode  "exit protected mode".
^ self _error: #rtErrUserIdAlreadyExists args: #[userId]
%

category: 'Updating'
method: UserProfileSet
_oldUserId: oldUserId newUserId: newUserId for: aUserProfile

"Private."

<protected>

| oldUserPro duplicateUser |

self _validatePrivilege.
duplicateUser := self userWithId: newUserId ifAbsent:[ nil ].
duplicateUser ~~ nil ifTrue:[
  System _disableProtectedMode  "exit protected mode".
  ^ self _error: #rtErrUserIdAlreadyExists args: #[ newUserId ]
  ].

oldUserPro := self userWithId: oldUserId .
oldUserPro == aUserProfile ifFalse:[
  System _disableProtectedMode  "exit protected mode".
  ^ self _halt: 'oldUserId does not match the given UserProfile' .
  ].

userIdDictionary removeKey: oldUserId .
userIdDictionary at: newUserId put: aUserProfile .
userSecurityData at: newUserId put: (userSecurityData at: oldUserId) .
userSecurityData removeKey: oldUserId .
%

category: 'Adding'
method: UserProfileSet
addNewUserWithId: userIdString
password: passwordString
defaultSegment: aSegment
privileges: anArrayOfStrings
inGroups: aCollectionOfGroupStrings
compilerLanguage: aLangString

"Creates a new UserProfile with the associated characteristics and adds it to
 AllUsers.  Generates an error if the userIdString duplicates the userId
 of any existing element of the receiver.  Returns the new UserProfile.

 In order to log in to GemStone, the user must be
 authorized to read and write in the specified default Segment.
 aSegment must be a committed Segment or nil.
 Pass nil as the value of aSegment to have the default Segment be
 the world-write segment for applications not using object-level security.

 Requires OtherPassword privilege.
"

"Create a new user profile and add it to AllUsers."
self _validatePrivilege.
^ UserProfile newWithUserId: userIdString
                       password: passwordString
                       defaultSegment: aSegment
                       privileges: anArrayOfStrings
                       inGroups: aCollectionOfGroupStrings
                       compilerLanguage: aLangString.
%

category: 'Adding'
method: UserProfileSet
addNewUserWithId: userIdString 
password: passwordString
defaultSegment: aSegment

"Creates a new UserProfile and adds it to AllUsers.  The new UserProfile
 has no privileges, and belongs to no groups.  

 aSegment is used as the defaultSegment of the new UserProfile and
 must be a committed Segment or nil.
 Pass nil as the value of aSegment to have the default Segment be
 the world-write segment for applications not using object-level security.
"

^ self addNewUserWithId: userIdString
          password: passwordString
          defaultSegment: aSegment
          privileges:  #() 
          inGroups:  #() 
          compilerLanguage: #ASCII
%

category: 'Adding'
method: UserProfileSet
addNewUserWithId: userIdString
password: passwordString

"Creates a new UserProfile and adds it to the receiver.  The new UserProfile
 has no privileges, and belongs to no groups.  This method creates a new
 Segment, which is owned by the new user and assigned as the user's default
 segment.  The new Segment is created with world-read permission.

 This default method can be used by the data curator in batch user
 installations.  Returns the new UserProfile.

 The current session must be in a transaction with no uncommitted changes 
 or an error will be generated.
 This method will writeLock AllUsers and SystemRepository for
 the duration of the method, so that a new Segment can be committed
 and then used in creation of the new UserProfile .
 If a writeLock cannot be obtained on AllUsers or SystemRepository , 
 an error will be generated.
 This method will commit its changes.

 Requires OtherPassword privilege.
"

| newSeg newUp oldUp |
self _validatePrivilege.
oldUp := self userWithId: userIdString ifAbsent:[ nil ].
oldUp == nil ifFalse:[
  self _error: #rtErrUserIdAlreadyExists args: #[ userIdString ].
].
System needsCommit ifTrue: [ self _error: #rtErrAbortWouldLoseData ].
System writeLock: self 
  ifDenied:[ self error:'unable to writeLock AllUsers' ]
  ifChanged:[ System beginTransaction ]. 
System writeLock: SystemRepository 
  ifDenied:[ System removeLocksForSession.
             self error:'unable to writeLock SystemRepository' ]
  ifChanged:[ System beginTransaction ]. 
newSeg := Segment newInRepository: SystemRepository .
newSeg worldAuthorization: #read .
System commitTransaction ifFalse:[ 
  System removeLocksForSession .
  System abortTransaction.
  self error:'commit of new Segment failed' 
].
newUp := self addNewUserWithId: userIdString
          password: passwordString
          defaultSegment: newSeg
          privileges:  #()
          inGroups:  #()
          compilerLanguage: #ASCII.
newSeg owner: newUp.
System commitAndReleaseLocks ifFalse:[ 
  System removeLocksForSession .
  System abortTransaction.
  self error:'commit of new UserProfile failed' 
].
^ newUp
%


category: 'Adding'
method: UserProfileSet
addNewUserWithId: userIdString
password: passwordString
defaultSegment: aSegment
privileges: anArrayOfStrings
inGroups: aCollectionOfGroupStrings

"Creates and returns a new UserProfile with the associated characteristics, and
 adds it to AllUsers.  Generates an error if the userIdString duplicates
 the userId of any existing element of the receiver.

 aSegment must be a committed Segment or nil.
 Pass nil as the value of aSegment to have the default Segment be
 the world-write segment for applications not using object-level security.
"

^ self addNewUserWithId: userIdString
  password: passwordString
  defaultSegment: aSegment
  privileges: anArrayOfStrings
  inGroups: aCollectionOfGroupStrings
  compilerLanguage: #ASCII
%

category: 'Disk Space Management'
method: UserProfileSet
findObjectsLargerThan: aSize limit: aLimit

"Searches the symbol list of each user in the receiver for named objects larger
 than aSize.  Returns an Array of the form #[ #[aUserId, aKey, anObject] ]
 where aKey is the symbolic representation of anObject such that:

 ((AllUsers userWithId: aUserId)
   resolveSymbol: aKey ) value == anObject

 is true.  For each user in the receiver, the search continues until there are
 no more named objects in the user's symbol list, or until the size of the
 result reaches the specified maximum aLimit.

 Generates an error if an authorization violation occurs."

| test result optimizeSet theUserId aUser aSymList aDict |

aSize _validateClass: SmallInteger.
aLimit _validateClass: SmallInteger.

test := Array new.
result := Array new.
optimizeSet := IdentitySet new.

1 to: self size do:[:j|
    aUser := self _at: j .
    theUserId := aUser userId.
    aSymList := aUser symbolList .
    1 to: aSymList size do:[:k|
          aDict := aSymList at: k .
          (optimizeSet includesIdentical: aDict) "avoid repeated scans of
                                                  shared Dictionaries"
          ifFalse:
            [ optimizeSet add: aDict.
              aDict associationsDo: [:anAssoc | | theVal |
                theVal := anAssoc value.
                  (theVal size > aSize)
                  ifTrue:
                    [ (result size >= aLimit)
                      ifTrue:
                        [ ^result ].
                      (test includesIdentical: theVal)
                      ifFalse:
                        [ test add: theVal.
                          result add: #[ theUserId, anAssoc key, theVal ].
                        ].
                   ].  "ifTrue: "
                ].  "aDict do: "
            ].  "ifFalse: "
      ].  "aUser symbolList do:"
  ].  "AllUsers do: "

^result
%

category: 'Private'
method: UserProfileSet
_remove: aUserProfile 

""
<protected>

self _validatePrivilege.
"use _removeKey: to avoid accessing value in userSecurityData, fix 36312"
userSecurityData _removeKey: aUserProfile userId .   
super _remove: aUserProfile
%

category: 'Removing'
method: UserProfileSet
remove: anObject ifAbsent: aBlock

"Reimplemented to maintain the userId dictionary.
 Requires OtherPassword privilege."

<primitive: 901>
| res userId |
self _validatePrivilege.
userIdDictionary ~~ nil ifTrue:[
  "protect against removal of special users"
  userId := anObject userId.
  (UserProfile isSpecialUserId: userId) ifTrue:[
    userId _error: #rtErrRemoveSpecialUser
    ].

  res := userIdDictionary removeKey: userId ifAbsent: [nil] .
  res ~~ nil ifTrue:[ 
     self _remove: anObject  .
     System _disableProtectedMode  "exit protected mode".
     ^ anObject
     ]
  ifFalse:[ 
    System _disableProtectedMode  "exit protected mode".
    ^ aBlock value 
    ].
  ].
System _disableProtectedMode  "exit protected mode".
^ super remove: anObject ifAbsent: aBlock
%


category: 'Group Membership'
method: UserProfileSet
addGroup: aGroupString

"Adds all the users in the receiver to the group represented by aGroupString.
 If the current session does not have the authorizations required for this
 operation, raises an error.
 Requires OtherPassword privilege."

self _validatePrivilege.
self do:[ :aUserProfile | aUserProfile addGroup: aGroupString ]
%

category: 'Group Membership'
method: UserProfileSet
removeGroup: aGroupString

"Removes all the users in the receiver from the group represented by
 aGroupString.  If the current session does not have the authorizations required
 for this operation, raises an error.
 Requires OtherPassword privilege."

self _validatePrivilege.
self do:[ :aUserProfile | aUserProfile removeGroup: aGroupString ].
%

category: 'Group Membership'
method: UserProfileSet
usersInGroup: aGroupString

"Returns all the elements of the receiver that are in the group represented by
 aGroupString.  If the current session does not have the authorizations required
 for this operation, raises an error."

| result theGroup |
theGroup := AllGroups _validateGroupString: aGroupString .
result := self class new .
self do:[:aUserProfile |
  (aUserProfile groups includesIdentical: theGroup) ifTrue:[
    result add: aUserProfile .
    ].
  ].
^ result
%

category: 'Accessing'
method: UserProfileSet
maxPasswordSize

"Returns the value of the maxPasswordSize instance variable."

^ maxPasswordSize
%

category: 'Accessing'
method: UserProfileSet
minPasswordSize

"Returns the value of the minPasswordSize instance variable."

^ minPasswordSize
%

category: 'Accessing'
method: UserProfileSet
maxRepeatingChars

"Returns the value of the maxRepeatingChars instance variable."

^ maxRepeatingChars
%

category: 'Accessing'
method: UserProfileSet
maxConsecutiveChars

"Returns the value of the maxConsecutiveChars instance variable."

^ maxConsecutiveChars
%

category: 'Accessing'
method: UserProfileSet
maxCharsOfSameType

"Returns the value of the maxCharsOfSameType instance variable."

^ maxCharsOfSameType
%

category: 'Accessing'
method: UserProfileSet
passwordAgeLimit

"Returns the value of the passwordAgeLimit instance variable."

^ passwordAgeLimit
%

category: 'Accessing'
method: UserProfileSet
passwordAgeWarning

"Returns the value of the passwordAgeWarning instance variable."

^ passwordAgeWarning
%

category: 'Accessing'
method: UserProfileSet
disallowUsedPasswords

"Returns the value of the disallowUsedPasswords instance variable."

^ disallowUsedPasswords
%

category: 'Accessing'
method: UserProfileSet
staleAccountAgeLimit

"Returns the value of the staleAccountAgeLimit instance variable."

^ staleAccountAgeLimit
%

category: 'Updating'
method: UserProfileSet
maxPasswordSize: aPositiveInteger

"Sets the value of the maxPasswordSize instance variable.
 Requires OtherPassword privilege."

self _validatePrivilege.
aPositiveInteger _validateClass: SmallInteger .
aPositiveInteger < 0 ifTrue:[ 
  aPositiveInteger _error: #rtErrArgNotPositive 
  ].

maxPasswordSize := aPositiveInteger
%

category: 'Updating'
method: UserProfileSet
minPasswordSize: aPositiveInteger

"Sets the value of the minPasswordSize instance variable.
 Requires OtherPassword privilege."

self _validatePrivilege.
aPositiveInteger _validateClass: SmallInteger .
aPositiveInteger < 0 ifTrue:[ 
  aPositiveInteger _error: #rtErrArgNotPositive 
  ].

minPasswordSize := aPositiveInteger
%

category: 'Updating'
method: UserProfileSet
maxRepeatingChars: aPositiveInteger

"Sets the value of the maxRepeatingChars instance variable.
 Requires OtherPassword privilege."

self _validatePrivilege.
aPositiveInteger _validateClass: SmallInteger .
aPositiveInteger < 0 ifTrue:[ 
  aPositiveInteger _error: #rtErrArgNotPositive 
  ].

maxRepeatingChars := aPositiveInteger
%

category: 'Updating'
method: UserProfileSet
maxConsecutiveChars: aPositiveInteger

"Sets the value of the maxConsecutiveChars instance variable.
 Requires OtherPassword privilege."

self _validatePrivilege.
aPositiveInteger _validateClass: SmallInteger .
aPositiveInteger < 0 ifTrue:[ 
  aPositiveInteger _error: #rtErrArgNotPositive 
  ].

maxConsecutiveChars := aPositiveInteger
%

category: 'Updating'
method: UserProfileSet
maxCharsOfSameType: aPositiveInteger

"Sets the value of the maxCharsOfSameType instance variable.
 Requires OtherPassword privilege."

self _validatePrivilege.
aPositiveInteger _validateClass: SmallInteger .
aPositiveInteger < 0 ifTrue:[ 
  aPositiveInteger _error: #rtErrArgNotPositive 
  ].

maxCharsOfSameType := aPositiveInteger
%

category: 'Updating'
method: UserProfileSet
passwordAgeLimit: numberOfHours

"If numberOfHours is greater than zero, the passwords of all UserProfiles in 
 the receiver other than those for SystemUser, SymbolUser, DataCurator, and GcUser 
 will expire the specified number of hours after they are last changed.

 The argument numberOfHours must be a SmallInteger or a Float and must be at
 least zero and at most 536870911.   Requires OtherPassword privilege."

 | message |

  self _validatePrivilege.
  numberOfHours _validateClasses:#[SmallInteger, Float]. 
  numberOfHours < 0 ifTrue:[
    numberOfHours _error: #rtErrArgNotPositive 
    ].
  numberOfHours > 536870911 ifTrue:[
    numberOfHours _error: #rtErrArgOutOfRange 
    ].

  passwordAgeLimit := numberOfHours .

  message := 
'      passwordAgeLimit for AllUsers changed to ' 
   , numberOfHours asString , ' hours.' .
  self addMsgToSecurityLog: message .
%

category: 'Updating'
method: UserProfileSet
passwordAgeWarning: numberOfHours

"If numberOfHours is greater than zero, warning of passwords about to expire
 will be given for logins that occur less than the specified number of hours 
 before the password is to expire.

 The argument numberOfHours must be a SmallInteger or a Float and must be at
 least zero and at most 536870911.
 Requires OtherPassword privilege."

  self _validatePrivilege.
  numberOfHours _validateClasses:#[SmallInteger, Float]. 
  numberOfHours < 0 ifTrue:[
    numberOfHours _error: #rtErrArgNotPositive 
    ].
  numberOfHours > 536870911 ifTrue:[
    numberOfHours _error: #rtErrArgOutOfRange 
    ].

  passwordAgeWarning := numberOfHours
%

category: 'Updating'
method: UserProfileSet
staleAccountAgeLimit: numberOfHours

"If numberOfHours is greater than zero, the password for an account is
 disabled if the user does not login to the account at least as often as
 the specified number of hours.  The users SystemUser, DataCurator, and GcUser
 are never disabled by this mechanism.

 The argument numberOfHours must be a SmallInteger or a Float and must be at
 least zero and at most 536870911.
 Requires OtherPassword privilege."

 | message |
  self _validatePrivilege.
  numberOfHours _validateClasses:#[SmallInteger, Float]. 
  numberOfHours < 0 ifTrue:[
    numberOfHours _error: #rtErrArgNotPositive 
    ].
  numberOfHours > 536870911 ifTrue:[
    numberOfHours _error: #rtErrArgOutOfRange 
    ].

  staleAccountAgeLimit := numberOfHours .

  message := 
'      staleAccountAgeLimit for AllUsers changed to ' 
   , numberOfHours asString , ' hours.' .
  self addMsgToSecurityLog: message .
%

category: 'Logging'
method: UserProfileSet
addMsgToSecurityLog: aString

"This method adds the date, time and userId prefix to the specified message
 string and includes the resulting string in the security log.  In GemStone
 5.0, the Stone log file is the security log.

 Requires OtherPassword privilege. "

| message |

self _validatePrivilege.
message := String withAll:'    SecurityLog: ' .
message addAll:  DateTime now asString ; addAll: ', ';
        addAll: System myUserProfile userId ;  lf ;
        addAll: aString .
System addAllToStoneLog: message .
%

category: 'Querying'
method: UserProfileSet
findDisabledUsers

"Returns a SortedCollection of UserProfiles that are disabled.  The result
 includes users whose accounts have been disabled because their passwords have
 expired, or whose accounts were not used within the interval defined by the
 staleAccountAgeLimit, or who failed to login within the number of tries
 specified by the Stone configuration parameter.

 Generates an error if you do not have OtherPassword privilege."

| result |

result := SortedCollection sortBlock:[:a :b | a userId < b userId ].
self do:[ :aUserProfile | 
  aUserProfile isDisabled ifTrue:[ result add: aUserProfile ].
  ].
^ result .
%

category: 'Querying'
method: UserProfileSet
findProfilesWithAgingPassword

"Returns a SortedCollection of UserProfiles whose passwords will expire sooner
 than passwordAgeWarning hours from now.

 Generates an error if you do not have OtherPassword privilege."

| result nowDtime endWarningDtime |

result := SortedCollection sortBlock:[:a :b | a userId < b userId ] .

(((passwordAgeWarning ~~ nil _and:[ passwordAgeWarning > 0])
  _and:[ passwordAgeLimit ~~ nil]) _and:[ passwordAgeLimit > 0]) 
ifTrue:[
  nowDtime := DateTime now.
  endWarningDtime := nowDtime addHours: passwordAgeWarning .
  self do:[ :aUserProfile | | expireDtime |
    (aUserProfile passwordNeverExpires) ifFalse: [ 
      (aUserProfile lastPasswordChange ~~ nil) ifTrue:[
        expireDtime := aUserProfile lastPasswordChange addHours: passwordAgeLimit.
        expireDtime <= endWarningDtime ifTrue:[ result add: aUserProfile ].
      ].
    ].
  ].
].
^ result
%

category: 'Updating'
method: UserProfileSet
disallowUsedPasswords: aBoolean

"Sets the value of the disallowUsedPasswords instance variable.
 Requires OtherPassword privilege. "

self _validatePrivilege.
disallowUsedPasswords := aBoolean
%

category: 'Private'
method: UserProfileSet
_initialize

"Private.  Only SystemUser has permission to execute this successfully."

| saveArr |

System myUserProfile userId = 'SystemUser' ifFalse:[
  self _halt:'Only SystemUser may execute this method.'
  ].

userSecurityData == nil ifTrue:[
  Segment setCurrent: self segment while:[
    self size > 0 ifTrue:[
      saveArr := Array new .
      saveArr addAll: self . 
      saveArr do: [ :aProfile | self remove: aProfile ].
    ] .

    userSecurityData := StringKeyValueDictionary new .

    userIdDictionary == nil 
       ifTrue:[ userIdDictionary := StringKeyValueDictionary new .  ]
      ifFalse:[  self _halt:'inconsistent state' ].

    maxPasswordSize := 0 .
    minPasswordSize := 0 .
    maxRepeatingChars := 0 .
    maxConsecutiveChars := 0 .
    maxCharsOfSameType := 0 .

    disallowedPasswords := Set new .
    disallowUsedPasswords := false .
   
    passwordAgeLimit := nil .
    passwordAgeWarning := nil .
    staleAccountAgeLimit := nil .

    saveArr ~~ nil ifTrue:[
      saveArr do:[ :aProfile | self add: aProfile ].
    ].
  ].							"fix 12466"
].
%

category: 'Accessing'
method: UserProfileSet
disallowedPasswords

"Returns the set of disallowed passwords defined for the receiver."

 ^ disallowedPasswords
%

category: 'Private'
method: UserProfileSet
_validateNewPassword: aString

"Generates an error if aString is not a valid new password for the receiver;
 otherwise, returns the receiver."

<protected>
| subStr |

(maxPasswordSize > 0 _and:[ aString size > maxPasswordSize]) ifTrue:[
  System _disableProtectedMode  "exit protected mode".
  ^ aString _error: #rtMaxPasswordSize args:#[ maxPasswordSize ]
  ].
(minPasswordSize > 0 _and:[ aString size < minPasswordSize]) ifTrue:[
  System _disableProtectedMode  "exit protected mode".
  ^ aString _error: #rtMinPasswordSize args:#[ minPasswordSize ]
  ].
(maxConsecutiveChars > 0) ifTrue:[
  subStr := aString maxConsecutiveSubstring .
  subStr size > maxConsecutiveChars ifTrue:[
    System _disableProtectedMode  "exit protected mode".
    ^ aString _error: #rtMaxConsecutiveChars 
	    args:#[ maxConsecutiveChars , subStr ].
    ].
  ].
(maxRepeatingChars > 0) ifTrue:[
  subStr := aString maxRepeatingSubstring .
  subStr size > maxRepeatingChars ifTrue:[
    System _disableProtectedMode  "exit protected mode".
    ^ aString _error: #rtMaxRepeatingChars 
	    args:#[ maxRepeatingChars , subStr ].
    ].
  ].
(maxCharsOfSameType > 0) ifTrue:[
  subStr := aString maxSameTypeSubstring .
  subStr size > maxCharsOfSameType ifTrue:[
    System _disableProtectedMode  "exit protected mode".
    ^ aString _error: #rtMaxCharsOfSameType 
	    args:#[ maxCharsOfSameType , (subStr at: 1) _typeAsSymbol asString ,
                    subStr ] .
    ].
  ].
(disallowedPasswords includes: aString) ifTrue:[
  System _disableProtectedMode  "exit protected mode".
  ^ aString _error: #rtDisallowedPassword 
  ].

"disallowUsedPasswords tested by instance method in UserProfile."
%

category: 'Private'
method: UserProfileSet
_validatePrivilege

" You must have #OtherPassword privilege to modify UserProfileSets "

  System myUserProfile _validatePrivilegeName: #OtherPassword
%

! species... methods added with fix 31257
category: 'Enumerating'
method: UserProfileSet
speciesForCollect

"Returns a Class, an instance of which should be used as the result of
 collect: or other projections applied to the receiver."

^ IdentityBag
%
category: 'Enumerating'
method: UserProfileSet
speciesForSelect

"Returns a Class, an instance of which should be used as the result of
 collect: or other projections applied to the receiver."

^ IdentitySet
%
