<commit_before>package aaf.vhr

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

import aaf.base.identity.Subject
import org.apache.shiro.SecurityUtils

@ToString(includeNames=true, includes="id, login, cn, email")
@EqualsAndHashCode
class ManagedSubject {
  static auditable = true

  static final affiliations = [ 'affiliate',
                                'alum',
                                'employee',
                                'faculty',
                                'library-walk-in',
                                'member',
                                'staff',
                                'student'] as List

  String login
  String hash

  String apiKey               // Use for local account management context
  String eptidKey             // Used as input for EPTID generation so login changes don't impact us - should never be altered.

  // Password reset. Both codes required to be input, second provided via SMS or administrator
  String resetCode
  String resetCodeExternal

  Date accountExpires         

  // AAF Core
  String cn                   // oid:2.5.4.3
  String email                // oid:0.9.2342.19200300.100.1.3
  String sharedToken          // oid:1.3.6.1.4.1.27856.1.2.5
  String displayName          // oid:2.16.840.1.113730.3.1.241
  String eduPersonAssurance   // oid:1.3.6.1.4.1.5923.1.1.1.11
  String eduPersonAffiliation // oid:1.3.6.1.4.1.5923.1.1.1.1 - stored seperated by ; for IdP resolver simplification
  String eduPersonEntitlement // oid:1.3.6.1.4.1.5923.1.1.1.7 - stored seperated by ; for IdP resolver simplification
  
  // AAF Optional
  String givenName            // oid:2.5.4.42
  String surname              // oid:2.5.4.4
  String mobileNumber         // oid:0.9.2342.19200300.100.1.41
  String telephoneNumber      // oid:2.5.4.20
  String postalAddress        // oid:2.5.4.16
  String organizationalUnit   // oid:2.5.4.11

  boolean active = false
  boolean locked = false
  boolean blocked = false
  boolean archived = false

  Date dateCreated
  Date lastUpdated

  int failedLogins = 0
  int failedResets = 0

  List challengeResponse
  List emailReset

  static hasMany = [challengeResponse: ChallengeResponse,
                    emailReset: EmailReset,
                    invitations: ManagedSubjectInvitation,
                    stateChanges: StateChange]  

  static belongsTo = [organization:Organization,
                      group:Group]

  static constraints = {
    login nullable:true, blank: false, unique: true, size: 3..100,  validator: { val -> if (val?.contains(' ')) return 'value.contains.space' }
    hash nullable:true, blank:false, minSize:60, maxSize:60
    
    resetCode nullable:true
    resetCodeExternal nullable:true

    accountExpires nullable:true 

    email blank:false, unique:true, email:true
    cn validator: {val, obj ->
      return (val != null && val != '' && (val.count(' ') == 0 || val.count(' ') == 1))
    }
    sharedToken nullable:false, blank: false, unique: true
    eduPersonEntitlement nullable:true, blank:false

    eduPersonAssurance inList: ['urn:mace:aaf.edu.au:iap:id:1',
                                'urn:mace:aaf.edu.au:iap:id:2',
                                'urn:mace:aaf.edu.au:iap:id:3',
                                'urn:mace:aaf.edu.au:iap:id:4']

    eduPersonAffiliation nullable:false, blank:false, maxSize: 255

    mobileNumber nullable: true, blank: false, validator: validMobileNumber
    givenName nullable: true, blank: false          
    surname nullable: true, blank: false            
    telephoneNumber nullable: true, blank: false   
    postalAddress nullable: true, blank: false      
    organizationalUnit nullable: true, blank: false 

    organization nullable: false
    group nullable: false
  }

  static mapping = {
    eduPersonEntitlement type: "text"
  }

  public ManagedSubject() {
    eptidKey = aaf.vhr.crypto.CryptoUtil.randomAlphanumeric(12)
    apiKey = aaf.vhr.crypto.CryptoUtil.randomAlphanumeric(16)
  }

  String plainPassword
  String plainPasswordConfirmation
  static transients = ['plainPassword', 'plainPasswordConfirmation']

  public boolean canCreate(Group owner) {
    SecurityUtils.subject.isPermitted("app:administrator") || 
    ( SecurityUtils.subject.isPermitted("app:manage:organization:${owner.organization.id}:group:${owner.id}:managedsubject:create") 
      && owner.functioning() )
  }

  public boolean canMutate() {
    SecurityUtils.subject.isPermitted("app:administrator") || 
    ( SecurityUtils.subject.isPermitted("app:manage:organization:${organization.id}:group:${group.id}:managedsubject:${id}:edit") 
      && !archived && !blocked && group.functioning() )
  }

  public boolean canDelete() {
    SecurityUtils.subject.isPermitted("app:administrator")
  }

  public boolean canShow() {
    SecurityUtils.subject.isPermitted("app:manage:organization:${organization.id}:group:${group.id}:managedsubject:show")
  }

  public canChangePassword() {
    !locked && !blocked && !archived && organization?.functioning() && group?.functioning()
  }

  public boolean functioning() {
    active && !locked && !blocked && !archived && group?.functioning()
  }

  public boolean isExpired() {
    if(!accountExpires)
      return false

    def now = new Date()
    now.after(accountExpires)
  }

  public void setEptidKey(String eptidKey) {
    log.error ("Unable to set eptidKey. It is created automatically and is immutable.")
  }

  public void setResetCode(String resetCode) {
    if(resetCode)
      this.resetCode = cleanCode(resetCode)
    else
      this.resetCode = null
  }

  public void setResetCodeExternal(String resetCodeExternal) {<commit_msg>ManagedSubject: Make correct use of GORM events.<commit_after>package aaf.vhr

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

import aaf.base.identity.Subject
import org.apache.shiro.SecurityUtils

@ToString(includeNames=true, includes="id, login, cn, email")
@EqualsAndHashCode
class ManagedSubject {
  static auditable = true

  static final affiliations = [ 'affiliate',
                                'alum',
                                'employee',
                                'faculty',
                                'library-walk-in',
                                'member',
                                'staff',
                                'student'] as List

  String login
  String hash

  String apiKey               // Use for local account management context
  String eptidKey             // Used as input for EPTID generation so login changes don't impact us - should never be altered.

  // Password reset. Both codes required to be input, second provided via SMS or administrator
  String resetCode
  String resetCodeExternal

  Date accountExpires         

  // AAF Core
  String cn                   // oid:2.5.4.3
  String email                // oid:0.9.2342.19200300.100.1.3
  String sharedToken          // oid:1.3.6.1.4.1.27856.1.2.5
  String displayName          // oid:2.16.840.1.113730.3.1.241
  String eduPersonAssurance   // oid:1.3.6.1.4.1.5923.1.1.1.11
  String eduPersonAffiliation // oid:1.3.6.1.4.1.5923.1.1.1.1 - stored seperated by ; for IdP resolver simplification
  String eduPersonEntitlement // oid:1.3.6.1.4.1.5923.1.1.1.7 - stored seperated by ; for IdP resolver simplification
  
  // AAF Optional
  String givenName            // oid:192.168.127.12
  String surname              // oid:2.5.4.4
  String mobileNumber         // oid:0.9.2342.19200300.100.1.41
  String telephoneNumber      // oid:2.5.4.20
  String postalAddress        // oid:2.5.4.16
  String organizationalUnit   // oid:2.5.4.11

  boolean active = false
  boolean locked = false
  boolean blocked = false
  boolean archived = false

  Date dateCreated
  Date lastUpdated

  int failedLogins = 0
  int failedResets = 0

  List challengeResponse
  List emailReset

  static hasMany = [challengeResponse: ChallengeResponse,
                    emailReset: EmailReset,
                    invitations: ManagedSubjectInvitation,
                    stateChanges: StateChange]  

  static belongsTo = [organization:Organization,
                      group:Group]

  static constraints = {
    login nullable:true, blank: false, unique: true, size: 3..100,  validator: { val -> if (val?.contains(' ')) return 'value.contains.space' }
    hash nullable:true, blank:false, minSize:60, maxSize:60
    
    resetCode nullable:true
    resetCodeExternal nullable:true

    apiKey nullable:true, unique:true
    eptidKey nullable:true, unique:true

    accountExpires nullable:true 

    email blank:false, unique:true, email:true
    cn validator: {val, obj ->
      return (val != null && val != '' && (val.count(' ') == 0 || val.count(' ') == 1))
    }
    sharedToken nullable:false, blank: false, unique: true
    eduPersonEntitlement nullable:true, blank:false

    eduPersonAssurance inList: ['urn:mace:aaf.edu.au:iap:id:1',
                                'urn:mace:aaf.edu.au:iap:id:2',
                                'urn:mace:aaf.edu.au:iap:id:3',
                                'urn:mace:aaf.edu.au:iap:id:4']

    eduPersonAffiliation nullable:false, blank:false, maxSize: 255

    mobileNumber nullable: true, blank: false, validator: validMobileNumber
    givenName nullable: true, blank: false          
    surname nullable: true, blank: false            
    telephoneNumber nullable: true, blank: false   
    postalAddress nullable: true, blank: false      
    organizationalUnit nullable: true, blank: false 

    organization nullable: false
    group nullable: false
  }

  static mapping = {
    eduPersonEntitlement type: "text"
  }

  def beforeValidate() {
    if(!eptidKey)
      eptidKey = aaf.vhr.crypto.CryptoUtil.randomAlphanumeric(12)

    if(!apiKey)
      apiKey = aaf.vhr.crypto.CryptoUtil.randomAlphanumeric(16)
  }

  String plainPassword
  String plainPasswordConfirmation
  static transients = ['plainPassword', 'plainPasswordConfirmation']

  public boolean canCreate(Group owner) {
    SecurityUtils.subject.isPermitted("app:administrator") || 
    ( SecurityUtils.subject.isPermitted("app:manage:organization:${owner.organization.id}:group:${owner.id}:managedsubject:create") 
      && owner.functioning() )
  }

  public boolean canMutate() {
    SecurityUtils.subject.isPermitted("app:administrator") || 
    ( SecurityUtils.subject.isPermitted("app:manage:organization:${organization.id}:group:${group.id}:managedsubject:${id}:edit") 
      && !archived && !blocked && group.functioning() )
  }

  public boolean canDelete() {
    SecurityUtils.subject.isPermitted("app:administrator")
  }

  public boolean canShow() {
    SecurityUtils.subject.isPermitted("app:manage:organization:${organization.id}:group:${group.id}:managedsubject:show")
  }

  public canChangePassword() {
    !locked && !blocked && !archived && organization?.functioning() && group?.functioning()
  }

  public boolean functioning() {
    active && !locked && !blocked && !archived && group?.functioning()
  }

  public boolean isExpired() {
    if(!accountExpires)
      return false

    def now = new Date()
    now.after(accountExpires)
  }

  public void setEptidKey(String eptidKey) {
    log.error ("Unable to set eptidKey. It is created automatically and is immutable.")
  }

  public void setApiKey(String apiKey) {
    log.error ("Unable to set apiKey. It is created automatically and is immutable.")
  }


  public void setResetCode(String resetCode) {
    if(resetCode)
      this.resetCode = cleanCode(resetCode)
    else
      this.resetCode = null
  }

  public void setResetCodeExternal(String resetCodeExternal) {
{'fraction_non_alphanumeric': 0.10665186576722739, 'fraction_numerical': 0.02869097429766886, 'mean_word_length': 2.582747017436525, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 34, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3633878', 'n_tokens_mistral': 3742, 'n_tokens_neox': 3466, 'n_words': 790}