/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package grails.plugin.springsecurity.acl

import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import org.springframework.context.MessageSource
import org.springframework.security.acls.domain.AccessControlEntryImpl
import org.springframework.security.acls.domain.GrantedAuthoritySid
import org.springframework.security.acls.domain.ObjectIdentityImpl
import org.springframework.security.acls.domain.PrincipalSid
import org.springframework.security.acls.jdbc.LookupStrategy
import org.springframework.security.acls.model.AccessControlEntry
import org.springframework.security.acls.model.Acl
import org.springframework.security.acls.model.AclCache
import org.springframework.security.acls.model.AlreadyExistsException
import org.springframework.security.acls.model.AuditableAccessControlEntry
import org.springframework.security.acls.model.ChildrenExistException
import org.springframework.security.acls.model.MutableAcl
import org.springframework.security.acls.model.MutableAclService
import org.springframework.security.acls.model.NotFoundException
import org.springframework.security.acls.model.ObjectIdentity
import org.springframework.security.acls.model.Sid
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.util.Assert

import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import org.grails.datastore.gorm.GormEntity

/**
 * GORM implementation of {@link org.springframework.security.acls.model.AclService} and {@link MutableAclService}.
 * Ported from <code>JdbcAclService</code> and <code>JdbcMutableAclService</code>.
 *
 * Individual methods are @Transactional since NotFoundException
 * is a runtime exception and will cause an unwanted transaction rollback
 *
 * @author <a href='mailto:burt@burtbeckwith.com'>Burt Beckwith</a>
 */
@CompileStatic
@Slf4j
class AclService implements MutableAclService, WarnErros {

    AclSidGormService aclSidGormService
    AclEntryGormService aclEntryGormService
    AclClassGormService aclClassGormService
    AclObjectIdentityGormService aclObjectIdentityGormService

    /** Dependency injection for aclLookupStrategy. */
    LookupStrategy aclLookupStrategy

    /** Dependency injection for aclCache. */
    AclCache aclCache

    /** Dependency injection for messageSource. */
    MessageSource messageSource

    @Transactional
    MutableAcl createAcl(ObjectIdentity objectIdentity) throws AlreadyExistsException {
        Assert.notNull objectIdentity, 'Object Identity required'

        // Check this object identity hasn't already been persisted
        if (aclObjectIdentityGormService.findByObjectIdentity(objectIdentity)) {
            throw new AlreadyExistsException("Object identity '$objectIdentity' already exists")
        }

        // Need to retrieve the current principal, in order to know who "owns" this ACL (can be changed later on)
        PrincipalSid sid = new PrincipalSid(SecurityContextHolder.context.authentication)

        // Create the acl_object_identity row
        createObjectIdentity objectIdentity, sid

        readAclById(objectIdentity) as MutableAcl
    }

    @Transactional
    protected AclObjectIdentity createObjectIdentity(ObjectIdentity object, Sid owner) {
        AclSid ownerSid = createOrRetrieveSid(owner, true)
        AclClass aclClass = createOrRetrieveClass(object.type, true)
        AclObjectIdentity aclObjectIdentity = new AclObjectIdentity(
                aclClass: aclClass,
                objectId: object.identifier as Long,
                owner: ownerSid,
                entriesInheriting: true)
        save(aclObjectIdentity)
    }

    @Transactional
    protected AclSid createOrRetrieveSid(Sid sid, boolean allowCreate) {
        Assert.notNull sid, 'Sid required'

        String sidName
        boolean principal
        if (sid instanceof PrincipalSid) {
            sidName = sid.principal
            principal = true
        } else if (sid instanceof GrantedAuthoritySid) {
            sidName = sid.grantedAuthority
            principal = false
        } else {
            throw new IllegalArgumentException('Unsupported implementation of Sid')
        }

        AclSid aclSid = aclSidGormService.findBySidAndPrincipal(sidName, principal)
        if (!aclSid && allowCreate) {
            aclSid = new AclSid(sid: sidName, principal: principal)
            save(aclSid)
        }
        aclSid
    }

    @Transactional
    protected AclClass createOrRetrieveClass(String className, boolean allowCreate) {
        AclClass aclClass = aclClassGormService.findByClassName(className)
        if (!aclClass && allowCreate) {
            aclClass = new AclClass(className: className)
            save(aclClass)
        }
        aclClass
    }

    @CompileDynamic
    @Transactional
    void deleteAcl(ObjectIdentity objectIdentity, boolean deleteChildren) throws ChildrenExistException {

        Assert.notNull objectIdentity, 'Object Identity required'
        Assert.notNull objectIdentity.identifier, "Object Identity doesn't provide an identifier"

        if (deleteChildren) {
            List<ObjectIdentity> children = findChildren(objectIdentity)
            if (children != null) {
                for (ObjectIdentity child : children) {
                    deleteAcl(child, true)
                }
            }
        }

        AclObjectIdentity oid = aclObjectIdentityGormService.findByObjectIdentity(objectIdentity)
        if (oid) {
            // Delete this ACL's ACEs in the acl_entry table
            deleteEntries oid

            // Delete this ACL's acl_object_identity row
            oid.delete(failOnError: true)

            AclEntry.withSession { it.flush() }
        }

        // Clear the cache
        aclCache.evictFromCache objectIdentity
    }

    @Transactional
    protected void deleteEntries(AclObjectIdentity oid) {
        if (oid) {
            List<Serializable> aclEntryIdList = aclEntryGormService.findAllIdByAclObjectIdentity(oid)
            List<AclEntry> entries = aclEntryIdList.collect { Serializable id -> AclEntry.load(id) }
            deleteEntries(entries)
        }
    }

    @CompileDynamic
    @Transactional
    protected void deleteEntries(List<AclEntry> entries) {
        log.debug 'Deleting entries: {}', entries
        if (entries) {
            for (AclEntry entry : entries) {

                entry.delete(failOnError: true)

            }
            AclEntry.withSession { it.flush() }
        }
    }

    @Transactional
    MutableAcl updateAcl(MutableAcl acl) throws NotFoundException {
        Assert.notNull acl.id, "Object Identity doesn't provide an identifier"

        AclObjectIdentity aclObjectIdentity = aclObjectIdentityGormService.findByObjectIdentity(acl.objectIdentity)

        List<AclEntry> existingAces = aclEntryGormService.findAllByAclObjectIdentity(aclObjectIdentity)

        List<AclEntry> toDelete = existingAces.findAll { AclEntry ace ->
            log.trace 'Checking ace for delete: {}', ace
            !acl.entries.find { AccessControlEntry entry ->
                log.trace 'Checking entry for delete: {}', entry
                entryEqual(ace, entry)
            }
        }

        List<AccessControlEntry> toCreate = acl.entries.findAll { AccessControlEntry entry ->
            log.trace 'Checking entry for create: {}', entry
            !existingAces.find { AclEntry ace ->
                log.trace 'Checking ace for create: {}', ace
                entryEqual(ace, entry)
            }
        }

        Integer maxAceOrder = existingAces.max { it.aceOrder }?.aceOrder

        // Delete this ACL's ACEs in the acl_entry table
        deleteEntries toDelete

        // Create this ACL's ACEs in the acl_entry table
        createEntries(acl, maxAceOrder, toCreate as List<AuditableAccessControlEntry>)

        // Change the mutable columns in acl_object_identity
        updateObjectIdentity acl

        // Clear the cache, including children
        clearCacheIncludingChildren acl.objectIdentity

        readAclById(acl.objectIdentity) as MutableAcl
    }

    @Transactional
    protected void createEntries(MutableAcl acl, Integer maxOrder, List<AuditableAccessControlEntry> entries) {
        int i = maxOrder != null ? maxOrder + 1 : 0
        for (AuditableAccessControlEntry entry in entries) {
            Assert.isInstanceOf AccessControlEntryImpl, entry, 'Unknown ACE class'
            AclEntry aclEntryInstance = new AclEntry(
                    aclObjectIdentity: AclObjectIdentity.load(acl.id),
                    aceOrder: i++,
                    sid: createOrRetrieveSid(entry.sid, true),
                    mask: entry.permission.mask,
                    granting: entry.isGranting(),
                    auditSuccess: entry.isAuditSuccess(),
                    auditFailure: entry.isAuditFailure())
            save(aclEntryInstance)
        }
    }

    @CompileDynamic
    @Transactional
    protected void updateObjectIdentity(MutableAcl acl) {
        Assert.notNull acl.owner, 'Owner is required in this implementation'

        AclObjectIdentity aclObjectIdentity = aclObjectIdentityGormService.findById(acl.id)

        AclObjectIdentity parent = null
        if (acl.parentAcl) {
            ObjectIdentity oii = acl.parentAcl.objectIdentity
            Assert.isInstanceOf ObjectIdentityImpl, oii, 'Implementation only supports ObjectIdentityImpl'
            parent = aclObjectIdentityGormService.findByObjectIdentity(oii)
        }
        aclObjectIdentity.parent = parent
        aclObjectIdentity.owner = createOrRetrieveSid(acl.owner, true)
        aclObjectIdentity.entriesInheriting = acl.isEntriesInheriting()
        save(aclObjectIdentity)
        AclObjectIdentity.withSession { it.flush() }
    }

    protected void clearCacheIncludingChildren(ObjectIdentity objectIdentity) {
        Assert.notNull objectIdentity, 'ObjectIdentity required'

        List<ObjectIdentity> children = findChildren(objectIdentity)
        for (ObjectIdentity child in children) {
            clearCacheIncludingChildren child
        }
        aclCache.evictFromCache objectIdentity
    }

    @ReadOnly
    List<ObjectIdentity> findChildren(ObjectIdentity parentOid) {
        List<AclObjectIdentity> children = aclObjectIdentityGormService.findAllByParentObjectIdAndParentAclClassName((parentOid?.identifier as Long), parentOid.type)

        if (!children) {
            return []
        }

        children.collect { AclObjectIdentity aoi ->
            new ObjectIdentityImpl(lookupClass(aoi.aclClass.className), aoi.objectId)
        }
    }

    protected Class<?> lookupClass(String className) {
        // workaround for Class.forName() not working in tests
        Class.forName className, true, Thread.currentThread().contextClassLoader
    }

    @ReadOnly(noRollbackFor = [NotFoundException])
    Acl readAclById(ObjectIdentity object) throws NotFoundException {
        readAclById object, null
    }

    @ReadOnly(noRollbackFor = [NotFoundException])
    Acl readAclById(ObjectIdentity object, List<Sid> sids) throws NotFoundException {
        Map<ObjectIdentity, Acl> map = readAclsById([object], sids)
        Assert.isTrue map.containsKey(object),
                "There should have been an Acl entry for ObjectIdentity $object"
        map[object]
    }

    @ReadOnly(noRollbackFor = [NotFoundException])
    Map<ObjectIdentity, Acl> readAclsById(List<ObjectIdentity> objects) throws NotFoundException {
        readAclsById objects, null
    }

    @ReadOnly(noRollbackFor = [NotFoundException])
    Map<ObjectIdentity, Acl> readAclsById(List<ObjectIdentity> objects, List<Sid> sids) throws NotFoundException {
        Map<ObjectIdentity, Acl> result = aclLookupStrategy.readAclsById(objects, sids)
        // Check every requested object identity was found (throw NotFoundException if needed)
        for (ObjectIdentity object in objects) {
            if (!result.containsKey(object)) {
                throw new NotFoundException("Unable to find ACL information for object identity '$object'")
            }
        }
        return result
    }

    @Transactional
    protected <T extends GormEntity<T>> T save(T bean) {
        if (!bean.save()) {
            log.warn errorsBeanBeingSaved(messageSource, bean)
        }
        bean
    }

    private static boolean entryEqual(AclEntry ace, AccessControlEntry entry) {
        Sid sid = ace.sid.principal ? new PrincipalSid(ace.sid.sid) : new GrantedAuthoritySid(ace.sid.sid)
        return entry.permission.mask == ace.mask && entry.sid == sid && entry.granting == ace.granting
    }
}
