├── .editorconfig ├── .github └── workflows │ ├── gradle.yml │ └── release.yml ├── .gitignore ├── .sdkmanrc ├── ISSUE_TEMPLATE.md ├── LICENSE.txt ├── README.md ├── build.gradle ├── examples ├── audit-test-allow-update-outside-transaction │ ├── README.md │ ├── build.gradle │ ├── grails-app │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── conf │ │ │ ├── application.groovy │ │ │ ├── application.yml │ │ │ ├── logback-spring.xml │ │ │ └── spring │ │ │ │ └── resources.groovy │ │ ├── domain │ │ │ └── test │ │ │ │ ├── AuditTrail.groovy │ │ │ │ ├── AuditTrailSecondDatasource.groovy │ │ │ │ ├── Author.groovy │ │ │ │ └── EntityInSecondDatastore.groovy │ │ └── init │ │ │ └── audit │ │ │ └── test │ │ │ └── Application.groovy │ └── src │ │ ├── integration-test │ │ └── groovy │ │ │ └── test │ │ │ └── AuditInsertWithoutTransactionSpec.groovy │ │ └── main │ │ └── groovy │ │ └── test │ │ └── TestUtils.groovy └── audit-test │ ├── README.md │ ├── build.gradle │ ├── grails-app │ ├── assets │ │ ├── images │ │ │ ├── apple-touch-icon-retina.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon.ico │ │ │ ├── grails_logo.png │ │ │ ├── skin │ │ │ │ ├── database_add.png │ │ │ │ ├── database_delete.png │ │ │ │ ├── database_edit.png │ │ │ │ ├── database_save.png │ │ │ │ ├── database_table.png │ │ │ │ ├── exclamation.png │ │ │ │ ├── house.png │ │ │ │ ├── information.png │ │ │ │ ├── shadow.jpg │ │ │ │ ├── sorted_asc.gif │ │ │ │ └── sorted_desc.gif │ │ │ └── spinner.gif │ │ ├── javascripts │ │ │ ├── application.js │ │ │ └── jquery-2.1.3.js │ │ └── stylesheets │ │ │ ├── application.css │ │ │ ├── errors.css │ │ │ ├── main.css │ │ │ └── mobile.css │ ├── conf │ │ ├── application.groovy │ │ ├── application.yml │ │ ├── logback-spring.xml │ │ └── spring │ │ │ └── resources.groovy │ ├── controllers │ │ ├── UrlMappings.groovy │ │ └── test │ │ │ ├── AuditTrailController.groovy │ │ │ └── AuthorController.groovy │ ├── domain │ │ └── test │ │ │ ├── Aircraft.groovy │ │ │ ├── Airport.groovy │ │ │ ├── AuditTrail.groovy │ │ │ ├── Author.groovy │ │ │ ├── Book.groovy │ │ │ ├── Coach.groovy │ │ │ ├── CompositeId.groovy │ │ │ ├── EntityInSecondDatastore.groovy │ │ │ ├── Heliport.groovy │ │ │ ├── NonAuditableCompositeId.groovy │ │ │ ├── Publisher.groovy │ │ │ ├── Resolution.groovy │ │ │ ├── Review.groovy │ │ │ ├── Runway.groovy │ │ │ ├── TestEntity.groovy │ │ │ ├── Train.groovy │ │ │ ├── Truck.groovy │ │ │ └── Tunnel.groovy │ ├── i18n │ │ └── messages.properties │ ├── init │ │ └── audit │ │ │ └── test │ │ │ └── Application.groovy │ └── views │ │ ├── auditTrail │ │ ├── index.gsp │ │ └── show.gsp │ │ ├── author │ │ ├── create.gsp │ │ ├── edit.gsp │ │ ├── index.gsp │ │ └── show.gsp │ │ ├── error.gsp │ │ ├── index.gsp │ │ ├── layouts │ │ └── main.gsp │ │ └── notFound.gsp │ └── src │ ├── integration-test │ └── groovy │ │ └── test │ │ ├── AuditDeleteSpec.groovy │ │ ├── AuditInsertSpec.groovy │ │ ├── AuditTransactionSpec.groovy │ │ ├── AuditTruncateSpec.groovy │ │ ├── AuditUpdateCollectionSpec.groovy │ │ ├── AuditUpdateSpec.groovy │ │ ├── AuditableSpec.groovy │ │ └── StampSpec.groovy │ └── main │ └── groovy │ └── test │ └── TestUtils.groovy ├── gradle.properties ├── gradle ├── docs.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin ├── build.gradle ├── grails-app │ ├── conf │ │ ├── DefaultAuditLogConfig.groovy │ │ ├── application.yml │ │ └── logback.groovy │ ├── controllers │ │ └── grails │ │ │ └── plugins │ │ │ └── orm │ │ │ └── auditable │ │ │ └── AuditLogEventController.groovy │ ├── init │ │ └── grails │ │ │ └── plugins │ │ │ └── orm │ │ │ └── auditable │ │ │ └── Application.groovy │ └── views │ │ └── auditLogEvent │ │ ├── list.gsp │ │ └── show.gsp └── src │ ├── docs │ ├── changelog.adoc │ ├── configuration.adoc │ ├── images │ │ └── cover.png │ ├── implementation.adoc │ ├── index.adoc │ ├── installation.adoc │ ├── introduction.adoc │ ├── stamping.adoc │ ├── templates │ │ └── index.tmpl │ ├── upgrading.adoc │ └── usage.adoc │ └── main │ ├── groovy │ └── grails │ │ └── plugins │ │ └── orm │ │ └── auditable │ │ ├── AuditEventType.groovy │ │ ├── AuditLogContext.groovy │ │ ├── AuditLogListener.groovy │ │ ├── AuditLogListenerUtil.groovy │ │ ├── AuditLogQueueManager.groovy │ │ ├── AuditLogTransactionSynchronization.groovy │ │ ├── AuditLoggingConfigUtils.groovy │ │ ├── AuditLoggingGrailsPlugin.groovy │ │ ├── Auditable.groovy │ │ ├── ReflectionUtils.groovy │ │ ├── StampListener.groovy │ │ ├── Stampable.groovy │ │ └── resolvers │ │ ├── AuditRequestResolver.groovy │ │ ├── DefaultAuditRequestResolver.groovy │ │ └── SpringSecurityRequestResolver.groovy │ ├── scripts │ └── audit-quickstart.groovy │ └── templates │ └── AuditLogEvent.groovy.template └── settings.gradle /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: "Java CI" 2 | on: 3 | push: 4 | branches: 5 | - '[5-9].[0-9].x' 6 | pull_request: 7 | branches: 8 | - '[5-9].[0-9].x' 9 | workflow_dispatch: 10 | env: 11 | GIT_USER_NAME: 'grails-build' 12 | GIT_USER_EMAIL: 'grails-build@users.noreply.github.com' 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | java: ['17', '21'] 19 | steps: 20 | - name: "📥 Checkout the repository" 21 | uses: actions/checkout@v4 22 | - name: "☕️ Setup JDK" 23 | uses: actions/setup-java@v4 24 | with: 25 | distribution: 'liberica' 26 | java-version: ${{ matrix.java }} 27 | - name: "🐘 Setup Gradle" 28 | uses: gradle/actions/setup-gradle@v4 29 | - name: "🔨 Run Base Tests" 30 | env: 31 | DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} 32 | DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_USER }} 33 | DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_KEY }} 34 | run: ./gradlew check --continue 35 | - name: "☄️ Upload Base Tests Results - audit-test" 36 | if: ${{ always() }} 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: testreport-audit-test-${{ matrix.java }} 40 | path: examples/audit-test/build/reports/tests 41 | - name: "☄️ Upload Base Tests Results - audit-test-allow-update-outside-transaction" 42 | if: ${{ always() }} 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: testreport-audit-test-allow-update-outside-transaction-${{ matrix.java }} 46 | path: examples/audit-test-allow-update-outside-transaction/build/reports/tests 47 | - name: "🔨 Run audit-test Outside of Transaction Tests" 48 | if: ${{ always() }} 49 | env: 50 | DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} 51 | DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_USER }} 52 | DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_KEY }} 53 | run: ./gradlew :examples:audit-test:check -Daudit-test.AuditTrail.datasource=DEFAULT --continue 54 | - name: "☄️ Upload audit-test Outside of Transaction Tests Results" 55 | if: ${{ always() }} 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: testreport-audit-test-single-datasource-${{ matrix.java }} 59 | path: examples/audit-test/build/reports/tests 60 | publish: 61 | if: github.event_name == 'push' 62 | needs: build 63 | runs-on: ubuntu-latest 64 | permissions: 65 | contents: read 66 | steps: 67 | - name: "📥 Checkout the repository" 68 | uses: actions/checkout@v4 69 | - name: "☕️ Setup JDK" 70 | uses: actions/setup-java@v4 71 | with: 72 | distribution: 'liberica' 73 | java-version: '17' 74 | - name: "🐘 Setup Gradle" 75 | uses: gradle/actions/setup-gradle@v4 76 | - name: "📤 Publish to Snapshot (repo.grails.org)" 77 | env: 78 | DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} 79 | MAVEN_PUBLISH_USERNAME: ${{ secrets.MAVEN_PUBLISH_USERNAME }} 80 | MAVEN_PUBLISH_PASSWORD: ${{ secrets.MAVEN_PUBLISH_PASSWORD }} 81 | MAVEN_PUBLISH_URL: ${{ secrets.MAVEN_PUBLISH_SNAPSHOT_URL }} 82 | working-directory: ./plugin 83 | run: ../gradlew publish 84 | - name: "📜 Generate Documentation" 85 | if: success() 86 | env: 87 | DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} 88 | working-directory: ./plugin 89 | run: ../gradlew docs 90 | - name: "🚀 Publish to Github Pages" 91 | if: success() 92 | uses: grails/github-pages-deploy-action@grails 93 | env: 94 | SKIP_SNAPSHOT: ${{ contains(needs.publish.outputs.release_version, 'M') }} 95 | TARGET_REPOSITORY: ${{ github.repository }} 96 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 97 | BRANCH: gh-pages 98 | FOLDER: plugin/build/docs 99 | DOC_FOLDER: gh-pages 100 | COMMIT_EMAIL: ${{ env.GIT_USER_EMAIL }} 101 | COMMIT_NAME: ${{ env.GIT_USER_NAME }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | on: 3 | release: 4 | types: [published] 5 | env: 6 | GIT_USER_NAME: 'grails-build' 7 | GIT_USER_EMAIL: 'grails-build@users.noreply.github.com' 8 | jobs: 9 | publish: 10 | permissions: 11 | contents: write # to create release 12 | issues: write # to modify milestones 13 | runs-on: ubuntu-latest 14 | outputs: 15 | release_version: ${{ steps.release_version.outputs.value }} 16 | target_branch: ${{ steps.extract_branch.outputs.value }} 17 | env: 18 | GIT_USER_NAME: 'grails-build' 19 | GIT_USER_EMAIL: 'grails-build@users.noreply.github.com' 20 | steps: 21 | - name: "📥 Checkout the repository" 22 | uses: actions/checkout@v4 23 | with: 24 | token: ${{ secrets.GH_TOKEN }} 25 | - name: "☕️ Setup JDK" 26 | uses: actions/setup-java@v4 27 | with: 28 | distribution: 'temurin' 29 | java-version: '17' 30 | - name: "🐘 Setup Gradle" 31 | uses: gradle/actions/setup-gradle@v4 32 | - name: "📝 Store the target branch" 33 | id: extract_branch 34 | run: | 35 | echo "Determining Target Branch" 36 | TARGET_BRANCH=${GITHUB_REF#refs/heads/} 37 | echo $TARGET_BRANCH 38 | echo "value=${TARGET_BRANCH}" >> $GITHUB_OUTPUT 39 | - name: "📝Set the current release version" 40 | id: release_version 41 | run: echo "value=${GITHUB_REF:11}" >> $GITHUB_OUTPUT 42 | - name: "⚙️ Run pre-release" 43 | uses: grails/github-actions/pre-release@main 44 | with: 45 | token: ${{ secrets.GITHUB_TOKEN }} 46 | - name: "🧩 Run Assemble" 47 | if: success() 48 | id: assemble 49 | env: 50 | DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_USER }} 51 | DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_KEY }} 52 | run: ./gradlew -U assemble 53 | - name: "📝 Generate secring file" 54 | env: 55 | SECRING_FILE: ${{ secrets.SECRING_FILE }} 56 | run: echo $SECRING_FILE | base64 -d > ${{ github.workspace }}/secring.gpg 57 | - name: "🚀 Publish to Sonatype OSSRH" 58 | id: publish 59 | env: 60 | NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} 61 | NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} 62 | NEXUS_PUBLISH_URL: ${{ secrets.NEXUS_PUBLISH_RELEASE_URL }} 63 | NEXUS_PUBLISH_STAGING_PROFILE_ID: ${{ secrets.NEXUS_PUBLISH_STAGING_PROFILE_ID }} 64 | SIGNING_KEY: ${{ secrets.SIGNING_KEY_ID }} 65 | SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} 66 | SECRING_FILE: ${{ secrets.SECRING_FILE }} 67 | DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_USER }} 68 | DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_KEY }} 69 | run: > 70 | ./gradlew 71 | -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg 72 | publishToSonatype 73 | closeSonatypeStagingRepository 74 | release: 75 | needs: publish 76 | runs-on: ubuntu-latest 77 | permissions: 78 | contents: read 79 | steps: 80 | - name: "📥 Checkout repository" 81 | uses: actions/checkout@v4 82 | - name: "☕️ Setup JDK" 83 | uses: actions/setup-java@v4 84 | with: 85 | distribution: liberica 86 | java-version: 17 87 | - name: "📥 Checkout repository" 88 | uses: actions/checkout@v4 89 | with: 90 | token: ${{ secrets.GH_TOKEN }} 91 | ref: v${{ needs.publish.outputs.release_version }} 92 | - name: "🐘 Setup Gradle" 93 | uses: gradle/actions/setup-gradle@v4 94 | with: 95 | develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} 96 | - name: "🏆Nexus Staging Close And Release" 97 | env: 98 | DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_USER }} 99 | DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_KEY }} 100 | NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} 101 | NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} 102 | NEXUS_PUBLISH_URL: ${{ secrets.NEXUS_PUBLISH_RELEASE_URL }} 103 | NEXUS_PUBLISH_STAGING_PROFILE_ID: ${{ secrets.NEXUS_PUBLISH_STAGING_PROFILE_ID }} 104 | run: > 105 | ./gradlew 106 | findSonatypeStagingRepository 107 | releaseSonatypeStagingRepository 108 | - name: "⚙️Run post-release" 109 | if: success() 110 | uses: grails/github-actions/post-release@main 111 | with: 112 | token: ${{ secrets.GITHUB_TOKEN }} 113 | env: 114 | SNAPSHOT_SUFFIX: -SNAPSHOT 115 | docs: 116 | needs: publish 117 | runs-on: ubuntu-latest 118 | permissions: 119 | contents: write 120 | steps: 121 | - name: "📥 Checkout the repository" 122 | uses: actions/checkout@v4 123 | with: 124 | token: ${{ secrets.GH_TOKEN }} 125 | ref: v${{ needs.publish.outputs.release_version }} 126 | - name: "☕️ Setup JDK" 127 | uses: actions/setup-java@v4 128 | with: 129 | distribution: 'liberica' 130 | java-version: '17' 131 | - name: "🐘 Setup Gradle" 132 | uses: gradle/actions/setup-gradle@v4 133 | - name: "📜 Generate Documentation" 134 | env: 135 | DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_USER }} 136 | DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.DEVELOCITY_BUILD_CACHE_NODE_KEY }} 137 | working-directory: ./plugin 138 | run: ../gradlew docs 139 | - name: "🚀 Publish to Github Pages" 140 | if: success() 141 | uses: grails/github-pages-deploy-action@v2 142 | env: 143 | SKIP_SNAPSHOT: ${{ contains(needs.publish.outputs.release_version, 'M') }} 144 | # if multiple releases are being done, this is the last branch - 1 version 145 | #SKIP_LATEST: ${{ !startsWith(needs.publish.outputs.target_branch, '6.2') }} 146 | TARGET_REPOSITORY: ${{ github.repository }} 147 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 148 | BRANCH: gh-pages 149 | FOLDER: plugin/build/docs 150 | DOC_FOLDER: gh-pages 151 | COMMIT_EMAIL: ${{ env.GIT_USER_EMAIL }} 152 | COMMIT_NAME: ${{ env.GIT_USER_NAME }} 153 | VERSION: ${{ needs.publish.outputs.release_version }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cobertura.ser 2 | *.log 3 | target/ 4 | build/ 5 | .gradle/ 6 | /docs 7 | plugin.xml 8 | *.zip 9 | .idea/ 10 | *.iml 11 | *.iws 12 | *.ipr 13 | /out 14 | .settings 15 | .classpath 16 | .project 17 | .DS_Store 18 | grails-audit-logging-plugin/web-app 19 | 20 | /audit-test/atlassian-ide-plugin.xml 21 | 22 | /atlassian-ide-plugin.xml 23 | audit-logging/classes 24 | classes 25 | /.asscache 26 | /audit-test/.asscache 27 | .vscode/settings.json 28 | /plugin/out 29 | /examples/audit-test/out 30 | /.direnv 31 | -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # Enable auto-env through the sdkman_auto_env config - https://sdkman.io/usage#env 2 | java=17.0.15-librca 3 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thanks for reporting an issue for the grails audit-logging plugin. 2 | Please review the task list below before submitting the issue. 3 | 4 | > WARNING: Your issue report may be closed if the issue report is incomplete and does not include an example. Make sure the below tasks are completed! 5 | 6 | > NOTE: If you are unsure about something and the issue is more of a question, a better place to ask questions is on Stack Overflow (http://stackoverflow.com/tags/grails) or Slack (http://slack-signup.grails.org). DO NOT use the issue tracker to ask questions. 7 | 8 | ### Task List 9 | 10 | - [ ] Steps to reproduce provided 11 | - [ ] Stacktrace (if present) provided 12 | - [ ] Example that reproduces the problem uploaded to Github 13 | - [ ] Full description of the issue provided (see below) 14 | 15 | ### Steps to Reproduce 16 | 17 | 1. TODO 18 | 2. TODO 19 | 3. TODO 20 | 21 | ### Expected Behaviour 22 | 23 | Tell us what should happen 24 | 25 | ### Actual Behaviour 26 | 27 | Tell us what happens instead 28 | 29 | ### Environment Information 30 | 31 | - **Operating System**: TODO 32 | - **GORM Version:** TODO 33 | - **Grails Version (if using Grails):** TODO 34 | - **JDK Version:** TODO 35 | 36 | ### Example Application 37 | 38 | - TODO: link to github repository with example that reproduces the issue 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Grails Audit Logging Plugin 2 | === 3 | [![Java CI](https://github.com/grails-plugins/grails-audit-logging-plugin/actions/workflows/gradle.yml/badge.svg?event=push)](https://github.com/grails-plugins/grails-audit-logging-plugin/actions/workflows/gradle.yml) 4 | ![Grails 7 Compatible](https://img.shields.io/badge/Compatible-brightGreen?label=Grails%207&labelColor=grey) 5 | 6 | This plugin was originally maintained by [symentis](https://github.com/symentis) and was gracefully donated to the grails developers to maintain. 7 | 8 | ## Description 9 | 10 | The Audit Logging plugin for Grails adds generic event based Audit Logging to a Grails project. 11 | 12 | For older Grails versions, see "Supported Grails Versions" below. 13 | 14 | ## Documentation 15 | * For current release documentation, see [User Guide](https://grails-plugins.github.io/grails-audit-logging-plugin/6.0.x/) 16 | * For snapshot documentation, see [Snapshot User Guide](https://grails-plugins.github.io/grails-audit-logging-plugin/snapshot/) 17 | * For 4.x documentation, see [4.x User Guide](https://grails-plugins.github.io/grails-audit-logging-plugin/4.0.x/plugin.html) 18 | * For 3.x documentation, see [3.x User Guide](https://grails-plugins.github.io/grails-audit-logging-plugin/3.0.x/plugin.html) 19 | * For 2.x documentation, see [2.x User Guide](https://grails-plugins.github.io/grails-audit-logging-plugin/2.0.x/plugin.html) 20 | 21 | ## Grails versions 22 | * Grails 7.0.x: [6.0.x branch (6.0.x version)](https://github.com/grails-plugins/grails-audit-logging-plugin/tree/6.0.x) 23 | * Grails 4.0.10+: [5.0.x branch (5.0.x version)](https://github.com/grails-plugins/grails-audit-logging-plugin/tree/5.0.x) 24 | * Grails up to 4.0.9: [4.x_maintenance branch](https://github.com/grails-plugins/grails-audit-logging-plugin/tree/4.x_maintenance) 25 | * Grails 3.3.x: [3.x_maintenance branch](https://github.com/grails-plugins/grails-audit-logging-plugin/tree/3.x_maintenance) 26 | * Grails 3.0.x-3.2.x: [2.x_maintenance branch](https://github.com/grails-plugins/grails-audit-logging-plugin/tree/2.x_maintenance) 27 | * Grails 2.x: [1.x_maintenance branch](https://github.com/grails-plugins/grails-audit-logging-plugin/tree/1.x_maintenance) 28 | 29 | ## Moving to Maven Central 30 | This repositories new artifacts are currently moved to Maven Central, since Bintray shut down MAY/01/21. You can obtain the old artifacts from https://repo.grails.org. 31 | 32 | 33 | ## audit-quickstart 34 | You need to perform "grails audit-quickstart \ \" after installing this plugin's 2.0.x version or later. 35 | 36 | With this, you get an auditlog domain class in your project which is fully under your control. 37 | The domain name is registered in your application.groovy with key "grails.plugins.auditLog.auditDomainClassName". 38 | 39 | Example: 40 | 41 | ``` 42 | grails audit-quickstart org.example.myproject MyAuditLogEvent 43 | ``` 44 | 45 | ## Issue Management 46 | 47 | See [GitHub Issues](https://github.com/grails-plugins/grails-audit-logging-plugin/issues "Issues") 48 | 49 | ## Pull Requests 50 | Pull requests are highly appreciated and welcome! 51 | 52 | Please add integration tests for new features to the audit-test application. 53 | 54 | ## Contributors 55 | Special thanks to all the contributors (in alphabetical order): 56 | 57 | Aaron Long 58 | Alan Wikie 59 | Aldrin 60 | Andrey Zhuchkov 61 | Ankur Tripathi 62 | Burt Beckwith 63 | bzamora33 64 | Danny Casady 65 | Dennie de Lange 66 | Dhiraj Mahapatro 67 | Elmar Kretzer 68 | Felix Scheinost 69 | Fernando Cambarieri 70 | Graeme Rocher 71 | Jeff Palmer 72 | Jorge Aguilera 73 | Juergen Baumann 74 | Madhava Jay 75 | Matt Long 76 | Matthew A Stewart 77 | Paul Taylor 78 | Robert Oschwald 79 | Sami Mäkelä 80 | Sebastien Arbogast 81 | Semyon Atamas 82 | Shawn Hartsock 83 | Tom Crossland 84 | 85 | Project lead: TBD 86 | *** 87 | 88 | YourKit Java Profiler 89 | 90 | YourKit is kindly supporting Grails open source projects with its full-featured Java Profiler. 91 | YourKit, LLC is the creator of innovative and intelligent tools for profiling 92 | Java and .NET applications. Take a look at YourKit's leading software products: 93 | [YourKit Java Profiler](http://www.yourkit.com/java/profiler/index.jsp) and 94 | [YourKit .NET Profiler](http://www.yourkit.com/.net/profiler/index.jsp). 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { url = "https://repository.apache.org/content/groups/snapshots/" } 4 | maven { url = "https://repo.grails.org/grails/core" } 5 | } 6 | dependencies { 7 | classpath platform("org.apache.grails:grails-bom:$grailsVersion") 8 | classpath "org.apache.grails:grails-gradle-plugins" 9 | classpath "com.bertramlabs.plugins:asset-pipeline-gradle" 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | mavenCentral() 16 | maven { url = "https://repository.apache.org/content/groups/snapshots/" } 17 | maven { url = 'https://repo.grails.org/grails/core' } 18 | // mavenLocal() // for local testing, do not commit uncommented 19 | } 20 | } 21 | 22 | version = project.projectVersion 23 | group = "org.grails.plugins" 24 | 25 | subprojects { Project project -> 26 | project.version = project.projectVersion 27 | project.group = "org.grails.plugins" 28 | 29 | if(project.name.endsWith('audit-logging')) { 30 | apply plugin: "org.apache.grails.gradle.grails-publish" 31 | grailsPublish { 32 | githubSlug = 'grails-plugins/grails-audit-logging-plugin' 33 | license { 34 | name = 'Apache-2.0' 35 | } 36 | title = "Grails Audit-Logging Plugin" 37 | desc = "Grails Audit-Logging Plugin for Grails 7+" 38 | developers = [robertoschwald:"Robert Oschwald", longwa:"Aaron Long", elkr:"Elmar Kretzer", 39 | jamesfredley:"James Fredley", jdaugherty:"James Daugherty"] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/README.md: -------------------------------------------------------------------------------- 1 | This is a sample test project for audit-logging. 2 | 3 | You can run the tests by 4 | 5 | ./gradlew check 6 | 7 | In comparison to `examples/audit-test` this application tests support for applications that are using `hibernate.allow_update_outside_transaction = true` and are not using a second datasource for `AuditTrail`. 8 | 9 | -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/build.gradle: -------------------------------------------------------------------------------- 1 | version = project.projectVersion 2 | group = 'audit.test' 3 | 4 | apply plugin: 'war' 5 | apply plugin: 'eclipse' 6 | apply plugin: 'idea' 7 | apply plugin: 'org.apache.grails.gradle.grails-web' 8 | apply plugin: 'org.apache.grails.gradle.grails-gsp' 9 | apply plugin: 'asset-pipeline' 10 | 11 | assets { 12 | minifyJs = true 13 | minifyCss = true 14 | } 15 | 16 | configurations { 17 | developmentOnly 18 | runtimeOnlyClasspath { 19 | extendsFrom developmentOnly 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation platform("org.apache.grails:grails-bom:$grailsVersion") 25 | 26 | implementation "org.apache.grails:grails-core" 27 | implementation "org.apache.grails:grails-logging" 28 | implementation "org.apache.grails:grails-databinding" 29 | implementation "org.apache.grails:grails-i18n" 30 | implementation "org.apache.grails:grails-interceptors" 31 | implementation "org.apache.grails:grails-rest-transforms" 32 | implementation "org.apache.grails:grails-services" 33 | implementation "org.apache.grails:grails-url-mappings" 34 | implementation "org.apache.grails:grails-web-boot" 35 | implementation "org.apache.grails:grails-gsp" 36 | implementation "org.apache.grails:grails-data-hibernate5" 37 | 38 | implementation "org.hibernate:hibernate-ehcache:$hibernate5Version", { 39 | // exclude javax variant of hibernate-core 40 | exclude group: 'org.hibernate', module: 'hibernate-core' 41 | } 42 | implementation "org.jboss.spec.javax.transaction:jboss-transaction-api_1.3_spec:$jbossTransactionApiVersion", { 43 | // required for hibernate-ehcache to work with javax variant of hibernate-core excluded 44 | } 45 | 46 | implementation "org.apache.grails:grails-scaffolding" 47 | implementation "org.springframework.boot:spring-boot-autoconfigure" 48 | implementation "org.springframework.boot:spring-boot-starter" 49 | implementation "org.springframework.boot:spring-boot-starter-actuator" 50 | implementation "org.springframework.boot:spring-boot-starter-logging" 51 | implementation "org.springframework.boot:spring-boot-starter-tomcat" 52 | implementation "org.springframework.boot:spring-boot-starter-validation" 53 | console "org.apache.grails:grails-console" 54 | runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails" 55 | runtimeOnly "com.h2database:h2" 56 | runtimeOnly "org.apache.tomcat:tomcat-jdbc" 57 | runtimeOnly "org.fusesource.jansi:jansi" 58 | integrationTestImplementation testFixtures("org.apache.grails:grails-geb") 59 | testImplementation "org.apache.grails:grails-testing-support-datamapping" 60 | testImplementation "org.apache.grails:grails-testing-support-web" 61 | testImplementation "org.spockframework:spock-core" 62 | 63 | implementation project(":audit-logging") 64 | profile "org.apache.grails.profiles:web" 65 | } 66 | 67 | test { 68 | testLogging { 69 | showStandardStreams = true 70 | exceptionFormat = 'full' 71 | } 72 | } 73 | 74 | tasks.withType(Test).configureEach { Task it -> 75 | useJUnitPlatform() 76 | 77 | if (it.name == 'test') { 78 | it.testLogging { 79 | showStandardStreams = true 80 | exceptionFormat = 'full' 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test-allow-update-outside-transaction/grails-app/assets/.gitkeep -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/conf/application.groovy: -------------------------------------------------------------------------------- 1 | grails { 2 | plugin { 3 | auditLog { 4 | verbose = true 5 | excluded = ['version', 'lastUpdated', 'lastUpdatedBy'] 6 | logFullClassName = true 7 | failOnError = true 8 | mask = ['ssn'] 9 | logIds = true 10 | defaultActor = 'SYS' 11 | replacementPatterns = ["a.b": ""] 12 | truncateLength = 1000000 13 | } 14 | } 15 | } 16 | 17 | // Added by the Audit-Logging plugin: 18 | grails.plugin.auditLog.auditDomainClassName = 'test.AuditTrail' 19 | 20 | hibernate_second { 21 | cache.use_second_level_cache = true 22 | cache.use_query_cache = false 23 | cache.region.factory_class = 'org.hibernate.cache.ehcache.EhCacheRegionFactory' 24 | } 25 | -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/conf/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | grails: 3 | profile: web 4 | codegen: 5 | defaultPackage: audit.test 6 | info: 7 | app: 8 | name: '@info.app.name@' 9 | version: '@info.app.version@' 10 | grailsVersion: '@info.app.grailsVersion@' 11 | spring: 12 | groovy: 13 | template: 14 | check-template-location: false 15 | 16 | --- 17 | grails: 18 | mime: 19 | disable: 20 | accept: 21 | header: 22 | userAgents: 23 | - Gecko 24 | - WebKit 25 | - Presto 26 | - Trident 27 | types: 28 | all: '*/*' 29 | atom: application/atom+xml 30 | css: text/css 31 | csv: text/csv 32 | form: application/x-www-form-urlencoded 33 | html: 34 | - text/html 35 | - application/xhtml+xml 36 | js: text/javascript 37 | json: 38 | - application/json 39 | - text/json 40 | multipartForm: multipart/form-data 41 | pdf: application/pdf 42 | rss: application/rss+xml 43 | text: text/plain 44 | hal: 45 | - application/hal+json 46 | - application/hal+xml 47 | xml: 48 | - text/xml 49 | - application/xml 50 | urlmapping: 51 | cache: 52 | maxsize: 1000 53 | controllers: 54 | defaultScope: singleton 55 | converters: 56 | encoding: UTF-8 57 | views: 58 | default: 59 | codec: html 60 | gsp: 61 | encoding: UTF-8 62 | htmlcodec: xml 63 | codecs: 64 | expression: html 65 | scriptlets: html 66 | taglib: none 67 | staticparts: none 68 | dataSource: 69 | pooled: true 70 | driverClassName: "org.h2.Driver" 71 | username: "sa" 72 | password: "" 73 | hibernate: 74 | allow_update_outside_transaction: true 75 | cache: 76 | queries: false 77 | use_second_level_cache: true 78 | use_query_cache: false 79 | region: 80 | factory_class: 'org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory' 81 | 82 | environments: 83 | development: 84 | dataSource: 85 | dbCreate: "create-drop" 86 | url: "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 87 | dataSources: 88 | second: 89 | dbCreate: "update" 90 | url: "jdbc:h2:mem:testDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 91 | test: 92 | dataSource: 93 | dbCreate: "update" 94 | url: "jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 95 | dataSources: 96 | second: 97 | dbCreate: "update" 98 | url: "jdbc:h2:mem:testDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 99 | production: 100 | dataSource: 101 | dbCreate: "update" 102 | url: "jdbc:h2:prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 103 | properties: 104 | maxActive: -1 105 | minEvictableIdleTimeMillis: 1800000 106 | timeBetweenEvictionRunsMillis: 1800000 107 | numTestsPerEvictionRun: 3 108 | testOnBorrow: true 109 | testWhileIdle: true 110 | testOnReturn: false 111 | validationQuery: "SELECT 1" 112 | jdbcInterceptors: "ConnectionState" 113 | dataSources: 114 | second: 115 | dbCreate: "update" 116 | url: "jdbc:h2:mem:testDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 117 | -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/conf/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/conf/spring/resources.groovy: -------------------------------------------------------------------------------- 1 | // Place your Spring DSL code here 2 | beans = { 3 | } 4 | -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/domain/test/AuditTrail.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package test 20 | 21 | import groovy.transform.ToString 22 | 23 | /** 24 | * AuditTrails are reported to the AuditLog table. 25 | * This requires you to set up a table or allow 26 | * Grails to create a table for you. (e.g. DDL or db-migration plugin) 27 | */ 28 | @ToString(includes = 'id,className,actor,propertyName,oldValue,newValue') 29 | class AuditTrail implements Serializable { 30 | private static final long serialVersionUID = 1L 31 | 32 | String id 33 | Date dateCreated 34 | Date lastUpdated 35 | 36 | String actor 37 | String uri 38 | String className 39 | String persistedObjectId 40 | Long persistedObjectVersion = 0 41 | 42 | String eventName 43 | String propertyName 44 | String oldValue 45 | String newValue 46 | 47 | static constraints = { 48 | actor(nullable: true) 49 | uri(nullable: true) 50 | className(nullable: true) 51 | persistedObjectId(nullable: true) 52 | persistedObjectVersion(nullable: true) 53 | eventName(nullable: true) 54 | propertyName(nullable: true) 55 | 56 | oldValue(nullable: true) 57 | newValue(maxSize: 10000, nullable: true) 58 | 59 | // for large column support (as in < 1.0.6 plugin versions), use 60 | // oldValue(nullable: true, maxSize: 65534) 61 | // newValue(nullable: true, maxSize: 65534) 62 | } 63 | 64 | static mapping = { 65 | table 'audit_log' 66 | 67 | cache usage: 'read-only', include: 'non-lazy' 68 | 69 | 70 | // no HQL queries package name import (was default in 1.x version) 71 | //autoImport false 72 | 73 | // Since 2.0.0, mapping is not done by config anymore. Configure your ID mapping here. 74 | id generator: "uuid2", type: 'string', length: 36 75 | 76 | version false 77 | } 78 | 79 | static namedQueries = { 80 | forQuery { String q -> 81 | if (!q?.trim()) return // return all 82 | def queries = q.tokenize()?.collect { 83 | '%' + it.replaceAll('_', '\\\\_') + '%' 84 | } 85 | queries.each { query -> 86 | or { 87 | ilike 'actor', query 88 | ilike 'persistedObjectId', query 89 | ilike 'propertyName', query 90 | ilike 'oldValue', query 91 | ilike 'newValue', query 92 | } 93 | } 94 | } 95 | 96 | forDateCreated { Date date -> 97 | if (!date) return 98 | gt 'dateCreated', date 99 | } 100 | } 101 | 102 | /** 103 | * Deserializer that maps a stored map onto the object 104 | * assuming that the keys match attribute properties. 105 | */ 106 | private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { 107 | def map = input.readObject() 108 | map.each { k, v -> this."$k" = v } 109 | } 110 | 111 | /** 112 | * Because Closures do not serialize we can't send the constraints closure 113 | * to the Serialize API so we have to have a custom serializer to allow for 114 | * this object to show up inside a webFlow context. 115 | */ 116 | private void writeObject(ObjectOutputStream out) throws IOException { 117 | def map = [ 118 | id : id, 119 | dateCreated : dateCreated, 120 | lastUpdated : lastUpdated, 121 | 122 | actor : actor, 123 | uri : uri, 124 | className : className, 125 | persistedObjectId : persistedObjectId, 126 | persistedObjectVersion: persistedObjectVersion, 127 | 128 | eventName : eventName, 129 | propertyName : propertyName, 130 | oldValue : oldValue, 131 | newValue : newValue, 132 | ] 133 | out.writeObject(map) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/domain/test/AuditTrailSecondDatasource.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package test 20 | 21 | import groovy.transform.ToString 22 | 23 | /** 24 | * AuditTrails are reported to the AuditLog table. 25 | * This requires you to set up a table or allow 26 | * Grails to create a table for you. (e.g. DDL or db-migration plugin) 27 | */ 28 | @ToString(includes = 'id,className,actor,propertyName,oldValue,newValue') 29 | class AuditTrailSecondDatasource implements Serializable { 30 | private static final long serialVersionUID = 1L 31 | 32 | String id 33 | Date dateCreated 34 | Date lastUpdated 35 | 36 | String actor 37 | String uri 38 | String className 39 | String persistedObjectId 40 | Long persistedObjectVersion = 0 41 | 42 | String eventName 43 | String propertyName 44 | String oldValue 45 | String newValue 46 | 47 | static constraints = { 48 | actor(nullable: true) 49 | uri(nullable: true) 50 | className(nullable: true) 51 | persistedObjectId(nullable: true) 52 | persistedObjectVersion(nullable: true) 53 | eventName(nullable: true) 54 | propertyName(nullable: true) 55 | 56 | oldValue(nullable: true) 57 | newValue(maxSize: 10000, nullable: true) 58 | 59 | // for large column support (as in < 1.0.6 plugin versions), use 60 | // oldValue(nullable: true, maxSize: 65534) 61 | // newValue(nullable: true, maxSize: 65534) 62 | } 63 | 64 | static mapping = { 65 | table 'audit_log' 66 | 67 | cache usage: 'read-only', include: 'non-lazy' 68 | 69 | datasource("second") 70 | 71 | // no HQL queries package name import (was default in 1.x version) 72 | //autoImport false 73 | 74 | // Since 2.0.0, mapping is not done by config anymore. Configure your ID mapping here. 75 | id generator: "uuid2", type: 'string', length: 36 76 | 77 | version false 78 | } 79 | 80 | static namedQueries = { 81 | forQuery { String q -> 82 | if (!q?.trim()) return // return all 83 | def queries = q.tokenize()?.collect { 84 | '%' + it.replaceAll('_', '\\\\_') + '%' 85 | } 86 | queries.each { query -> 87 | or { 88 | ilike 'actor', query 89 | ilike 'persistedObjectId', query 90 | ilike 'propertyName', query 91 | ilike 'oldValue', query 92 | ilike 'newValue', query 93 | } 94 | } 95 | } 96 | 97 | forDateCreated { Date date -> 98 | if (!date) return 99 | gt 'dateCreated', date 100 | } 101 | } 102 | 103 | /** 104 | * Deserializer that maps a stored map onto the object 105 | * assuming that the keys match attribute properties. 106 | */ 107 | private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { 108 | def map = input.readObject() 109 | map.each { k, v -> this."$k" = v } 110 | } 111 | 112 | /** 113 | * Because Closures do not serialize we can't send the constraints closure 114 | * to the Serialize API so we have to have a custom serializer to allow for 115 | * this object to show up inside a webFlow context. 116 | */ 117 | private void writeObject(ObjectOutputStream out) throws IOException { 118 | def map = [ 119 | id : id, 120 | dateCreated : dateCreated, 121 | lastUpdated : lastUpdated, 122 | 123 | actor : actor, 124 | uri : uri, 125 | className : className, 126 | persistedObjectId : persistedObjectId, 127 | persistedObjectVersion: persistedObjectVersion, 128 | 129 | eventName : eventName, 130 | propertyName : propertyName, 131 | oldValue : oldValue, 132 | newValue : newValue, 133 | ] 134 | out.writeObject(map) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/domain/test/Author.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class Author implements Auditable { 6 | String name 7 | Long age 8 | Boolean famous = false 9 | 10 | // This should get masked globally 11 | String ssn = "123-456-7890" 12 | 13 | Date dateCreated 14 | Date lastUpdated 15 | String lastUpdatedBy 16 | 17 | static constraints = { 18 | lastUpdatedBy nullable: true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/domain/test/EntityInSecondDatastore.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class EntityInSecondDatastore implements Auditable { 6 | 7 | String name 8 | Integer someIntegerProperty 9 | 10 | static constraints = { 11 | } 12 | 13 | static mapping = { 14 | datasource("second") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/grails-app/init/audit/test/Application.groovy: -------------------------------------------------------------------------------- 1 | package audit.test 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | 6 | class Application extends GrailsAutoConfiguration { 7 | static void main(String[] args) { 8 | GrailsApp.run(Application, args) 9 | } 10 | } -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/src/integration-test/groovy/test/AuditInsertWithoutTransactionSpec.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.AuditLogContext 4 | import grails.plugins.orm.auditable.AuditLoggingConfigUtils 5 | import grails.testing.mixin.integration.Integration 6 | import spock.lang.Shared 7 | import spock.lang.Specification 8 | 9 | @Integration 10 | class AuditInsertWithoutTransactionSpec extends Specification { 11 | 12 | @Shared 13 | def defaultIgnoreList 14 | 15 | void setup() { 16 | defaultIgnoreList = ['id'] + AuditLoggingConfigUtils.auditConfig.excluded?.asImmutable() ?: [] 17 | AuditTrail.withNewTransaction { 18 | AuditTrail.executeUpdate('delete from AuditTrail') 19 | } 20 | } 21 | 22 | def "test insert without transaction"() { 23 | Author.withNewSession { 24 | new Author(name: "Aaron", age: 37, famous: true).save(flush: true, failOnError: true) 25 | } 26 | 27 | expect: 28 | Author.withNewSession { 29 | def events = AuditTrail.findAllByClassName("test.Author") 30 | events.size() 31 | } == TestUtils.getAuditableProperties(Author.gormPersistentEntity, defaultIgnoreList).size() 32 | } 33 | 34 | def "test insert without transaction second datasource"() { 35 | AuditLogContext.withConfig(auditDomainClassName: AuditTrailSecondDatasource.canonicalName) { 36 | Author.withNewSession { 37 | new Author(name: "Aaron", age: 37, famous: true).save(flush: true, failOnError: true) 38 | } 39 | } 40 | 41 | expect: 42 | AuditTrailSecondDatasource.withNewSession { 43 | def events = AuditTrailSecondDatasource.findAllByClassName("test.Author") 44 | events.size() == TestUtils.getAuditableProperties(Author.gormPersistentEntity, defaultIgnoreList).size() 45 | } 46 | } 47 | 48 | def "test transaction synchronization is active but not for changed domain"() { 49 | expect: 50 | Author.withNewSession { 51 | EntityInSecondDatastore.withNewTransaction { 52 | new Author(name: "Aaron", age: 37, famous: true).save(flush: true, failOnError: true) 53 | 54 | // We have a transaction active but not for the session that the Author is saved in 55 | // => AuditTrail needs to be saved immediately 56 | AuditTrail.withNewSession { 57 | AuditTrail.count 58 | } 59 | } 60 | } > 0 61 | } 62 | } -------------------------------------------------------------------------------- /examples/audit-test-allow-update-outside-transaction/src/main/groovy/test/TestUtils.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import org.grails.datastore.mapping.model.PersistentEntity 4 | import org.grails.datastore.mapping.model.PersistentProperty 5 | 6 | class TestUtils { 7 | static getAuditableProperties(PersistentEntity entity, List ignoreList) { 8 | List properties = entity.persistentProperties.findResults { PersistentProperty p -> 9 | return p.name 10 | } 11 | return properties - ignoreList 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/audit-test/README.md: -------------------------------------------------------------------------------- 1 | This is a sample test project for audit-logging. 2 | 3 | You can either run the tests by 4 | 5 | grails test-app 6 | 7 | or start the application: 8 | 9 | grails run-app 10 | 11 | There you create Authors and list / query the AuditTrails entries. 12 | 13 | This sample application is used by CI to perform the integration tests. 14 | -------------------------------------------------------------------------------- /examples/audit-test/build.gradle: -------------------------------------------------------------------------------- 1 | version = project.projectVersion 2 | group = 'audit.test' 3 | 4 | apply plugin: 'war' 5 | apply plugin: 'eclipse' 6 | apply plugin: 'idea' 7 | apply plugin: 'org.apache.grails.gradle.grails-web' 8 | apply plugin: 'org.apache.grails.gradle.grails-gsp' 9 | apply plugin: 'asset-pipeline' 10 | 11 | assets { 12 | minifyJs = true 13 | minifyCss = true 14 | } 15 | 16 | configurations { 17 | developmentOnly 18 | runtimeOnlyClasspath { 19 | extendsFrom developmentOnly 20 | } 21 | } 22 | 23 | dependencies { 24 | 25 | implementation platform("org.apache.grails:grails-bom:$grailsVersion") 26 | 27 | developmentOnly "org.springframework.boot:spring-boot-devtools" 28 | implementation "org.apache.grails:grails-core" 29 | implementation "org.apache.grails:grails-logging" 30 | implementation "org.apache.grails:grails-databinding" 31 | implementation "org.apache.grails:grails-i18n" 32 | implementation "org.apache.grails:grails-interceptors" 33 | implementation "org.apache.grails:grails-rest-transforms" 34 | implementation "org.apache.grails:grails-services" 35 | implementation "org.apache.grails:grails-url-mappings" 36 | implementation "org.apache.grails:grails-web-boot" 37 | implementation "org.apache.grails:grails-gsp" 38 | implementation "org.apache.grails:grails-data-hibernate5" 39 | 40 | implementation "org.hibernate:hibernate-ehcache:$hibernate5Version", { 41 | // exclude javax variant of hibernate-core 42 | exclude group: 'org.hibernate', module: 'hibernate-core' 43 | } 44 | implementation "org.jboss.spec.javax.transaction:jboss-transaction-api_1.3_spec:$jbossTransactionApiVersion", { 45 | // required for hibernate-ehcache to work with javax variant of hibernate-core excluded 46 | } 47 | 48 | implementation "org.apache.grails:grails-scaffolding" 49 | implementation "org.springframework.boot:spring-boot-autoconfigure" 50 | implementation "org.springframework.boot:spring-boot-starter" 51 | implementation "org.springframework.boot:spring-boot-starter-actuator" 52 | implementation "org.springframework.boot:spring-boot-starter-logging" 53 | implementation "org.springframework.boot:spring-boot-starter-tomcat" 54 | implementation "org.springframework.boot:spring-boot-starter-validation" 55 | console "org.apache.grails:grails-console" 56 | runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails" 57 | runtimeOnly "com.h2database:h2" 58 | runtimeOnly "org.apache.tomcat:tomcat-jdbc" 59 | runtimeOnly "org.fusesource.jansi:jansi" 60 | integrationTestImplementation testFixtures("org.apache.grails:grails-geb") 61 | testImplementation "org.apache.grails:grails-testing-support-datamapping" 62 | testImplementation "org.apache.grails:grails-testing-support-web" 63 | testImplementation "org.spockframework:spock-core" 64 | 65 | implementation project(":audit-logging") 66 | profile "org.apache.grails.profiles:web" 67 | } 68 | 69 | tasks.withType(Test).configureEach { Task it -> 70 | useJUnitPlatform() 71 | 72 | if (it.name == 'test') { 73 | it.testLogging { 74 | showStandardStreams = true 75 | exceptionFormat = 'full' 76 | events = ["passed", "failed", "skipped", 'standardOut', 'standardError'] 77 | } 78 | } 79 | 80 | if (it.name == 'integrationTest') { 81 | // Added to pass on audit-test.AuditTrail.datasource 82 | it.systemProperties += System.properties 83 | it.testLogging { 84 | showStandardStreams = true 85 | exceptionFormat = 'full' 86 | events = ["passed", "failed", "skipped", 'standardOut', 'standardError'] 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/apple-touch-icon-retina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/apple-touch-icon-retina.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/favicon.ico -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/grails_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/grails_logo.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/database_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/database_add.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/database_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/database_delete.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/database_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/database_edit.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/database_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/database_save.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/database_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/database_table.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/exclamation.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/house.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/information.png -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/shadow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/shadow.jpg -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/sorted_asc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/sorted_asc.gif -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/skin/sorted_desc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/skin/sorted_desc.gif -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/examples/audit-test/grails-app/assets/images/spinner.gif -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js. 2 | // 3 | // Any JavaScript file within this directory can be referenced here using a relative path. 4 | // 5 | // You're free to add application-wide JavaScript to this file, but it's generally better 6 | // to create separate JavaScript files as needed. 7 | // 8 | //= require jquery-2.1.3.js 9 | //= require_tree . 10 | //= require_self 11 | 12 | if (typeof jQuery !== 'undefined') { 13 | (function($) { 14 | $('#spinner').ajaxStart(function() { 15 | $(this).fadeIn(); 16 | }).ajaxStop(function() { 17 | $(this).fadeOut(); 18 | }); 19 | })(jQuery); 20 | } 21 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS file within this directory can be referenced here using a relative path. 6 | * 7 | * You're free to add application-wide styles to this file and they'll appear at the top of the 8 | * compiled file, but it's generally better to create a new file per style scope. 9 | * 10 | *= require main 11 | *= require mobile 12 | *= require_self 13 | */ 14 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/stylesheets/errors.css: -------------------------------------------------------------------------------- 1 | h1, h2 { 2 | margin: 10px 25px 5px; 3 | } 4 | 5 | h2 { 6 | font-size: 1.1em; 7 | } 8 | 9 | .filename { 10 | font-style: italic; 11 | } 12 | 13 | .exceptionMessage { 14 | margin: 10px; 15 | border: 1px solid #000; 16 | padding: 5px; 17 | background-color: #E9E9E9; 18 | } 19 | 20 | .stack, 21 | .snippet { 22 | margin: 0 25px 10px; 23 | } 24 | 25 | .stack, 26 | .snippet { 27 | border: 1px solid #ccc; 28 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 29 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 30 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 31 | } 32 | 33 | /* error details */ 34 | .error-details { 35 | border-top: 1px solid #FFAAAA; 36 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 37 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 38 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 39 | border-bottom: 1px solid #FFAAAA; 40 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 41 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 42 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 43 | background-color:#FFF3F3; 44 | line-height: 1.5; 45 | overflow: hidden; 46 | padding: 5px; 47 | padding-left:25px; 48 | } 49 | 50 | .error-details dt { 51 | clear: left; 52 | float: left; 53 | font-weight: bold; 54 | margin-right: 5px; 55 | } 56 | 57 | .error-details dt:after { 58 | content: ":"; 59 | } 60 | 61 | .error-details dd { 62 | display: block; 63 | } 64 | 65 | /* stack trace */ 66 | .stack { 67 | padding: 5px; 68 | overflow: auto; 69 | height: 150px; 70 | } 71 | 72 | /* code snippet */ 73 | .snippet { 74 | background-color: #fff; 75 | font-family: monospace; 76 | } 77 | 78 | .snippet .line { 79 | display: block; 80 | } 81 | 82 | .snippet .lineNumber { 83 | background-color: #ddd; 84 | color: #999; 85 | display: inline-block; 86 | margin-right: 5px; 87 | padding: 0 3px; 88 | text-align: right; 89 | width: 3em; 90 | } 91 | 92 | .snippet .error { 93 | background-color: #fff3f3; 94 | font-weight: bold; 95 | } 96 | 97 | .snippet .error .lineNumber { 98 | background-color: #faa; 99 | color: #333; 100 | font-weight: bold; 101 | } 102 | 103 | .snippet .line:first-child .lineNumber { 104 | padding-top: 5px; 105 | } 106 | 107 | .snippet .line:last-child .lineNumber { 108 | padding-bottom: 5px; 109 | } -------------------------------------------------------------------------------- /examples/audit-test/grails-app/assets/stylesheets/mobile.css: -------------------------------------------------------------------------------- 1 | /* Styles for mobile devices */ 2 | 3 | @media screen and (max-width: 480px) { 4 | .nav { 5 | padding: 0.5em; 6 | } 7 | 8 | .nav li { 9 | margin: 0 0.5em 0 0; 10 | padding: 0.25em; 11 | } 12 | 13 | /* Hide individual steps in pagination, just have next & previous */ 14 | .pagination .step, .pagination .currentStep { 15 | display: none; 16 | } 17 | 18 | .pagination .prevLink { 19 | float: left; 20 | } 21 | 22 | .pagination .nextLink { 23 | float: right; 24 | } 25 | 26 | /* pagination needs to wrap around floated buttons */ 27 | .pagination { 28 | overflow: hidden; 29 | } 30 | 31 | /* slightly smaller margin around content body */ 32 | fieldset, 33 | .property-list { 34 | padding: 0.3em 1em 1em; 35 | } 36 | 37 | input, textarea { 38 | width: 100%; 39 | -moz-box-sizing: border-box; 40 | -webkit-box-sizing: border-box; 41 | -ms-box-sizing: border-box; 42 | box-sizing: border-box; 43 | } 44 | 45 | select, input[type=checkbox], input[type=radio], input[type=submit], input[type=button], input[type=reset] { 46 | width: auto; 47 | } 48 | 49 | /* hide all but the first column of list tables */ 50 | .scaffold-list td:not(:first-child), 51 | .scaffold-list th:not(:first-child) { 52 | display: none; 53 | } 54 | 55 | .scaffold-list thead th { 56 | text-align: center; 57 | } 58 | 59 | /* stack form elements */ 60 | .fieldcontain { 61 | margin-top: 0.6em; 62 | } 63 | 64 | .fieldcontain label, 65 | .fieldcontain .property-label, 66 | .fieldcontain .property-value { 67 | display: block; 68 | float: none; 69 | margin: 0 0 0.25em 0; 70 | text-align: left; 71 | width: auto; 72 | } 73 | 74 | .errors ul, 75 | .message p { 76 | margin: 0.5em; 77 | } 78 | 79 | .error ul { 80 | margin-left: 0; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/conf/application.groovy: -------------------------------------------------------------------------------- 1 | grails { 2 | plugin { 3 | auditLog { 4 | verbose = true 5 | excluded = ['version', 'lastUpdated', 'lastUpdatedBy'] 6 | logFullClassName = true 7 | failOnError = true 8 | mask = ['ssn'] 9 | logIds = true 10 | defaultActor = 'SYS' 11 | useDatasource = 'second' // store in "second" datasource 12 | replacementPatterns = ["a.b": ""] 13 | truncateLength = 1000000 14 | } 15 | } 16 | } 17 | 18 | // Added by the Audit-Logging plugin: 19 | grails.plugin.auditLog.auditDomainClassName = 'test.AuditTrail' 20 | 21 | hibernate_second { 22 | cache.use_second_level_cache = true 23 | cache.use_query_cache = false 24 | cache.region.factory_class = 'org.hibernate.cache.ehcache.EhCacheRegionFactory' 25 | } 26 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/conf/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | grails: 3 | profile: web 4 | codegen: 5 | defaultPackage: audit.test 6 | info: 7 | app: 8 | name: '@info.app.name@' 9 | version: '@info.app.version@' 10 | grailsVersion: '@info.app.grailsVersion@' 11 | spring: 12 | groovy: 13 | template: 14 | check-template-location: false 15 | 16 | --- 17 | grails: 18 | mime: 19 | disable: 20 | accept: 21 | header: 22 | userAgents: 23 | - Gecko 24 | - WebKit 25 | - Presto 26 | - Trident 27 | types: 28 | all: '*/*' 29 | atom: application/atom+xml 30 | css: text/css 31 | csv: text/csv 32 | form: application/x-www-form-urlencoded 33 | html: 34 | - text/html 35 | - application/xhtml+xml 36 | js: text/javascript 37 | json: 38 | - application/json 39 | - text/json 40 | multipartForm: multipart/form-data 41 | pdf: application/pdf 42 | rss: application/rss+xml 43 | text: text/plain 44 | hal: 45 | - application/hal+json 46 | - application/hal+xml 47 | xml: 48 | - text/xml 49 | - application/xml 50 | urlmapping: 51 | cache: 52 | maxsize: 1000 53 | controllers: 54 | defaultScope: singleton 55 | converters: 56 | encoding: UTF-8 57 | views: 58 | default: 59 | codec: html 60 | gsp: 61 | encoding: UTF-8 62 | htmlcodec: xml 63 | codecs: 64 | expression: html 65 | scriptlets: html 66 | taglib: none 67 | staticparts: none 68 | dataSource: 69 | pooled: true 70 | driverClassName: "org.h2.Driver" 71 | username: "sa" 72 | password: "" 73 | hibernate: 74 | cache: 75 | queries: false 76 | use_second_level_cache: true 77 | use_query_cache: false 78 | region: 79 | factory_class: 'org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory' 80 | 81 | environments: 82 | development: 83 | dataSource: 84 | dbCreate: "create-drop" 85 | url: "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 86 | dataSources: 87 | second: 88 | dbCreate: "update" 89 | url: "jdbc:h2:mem:testDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 90 | test: 91 | #we test with several configured datasources. See GPAUDITLOGGING-64 92 | dataSource: 93 | dbCreate: "update" 94 | url: "jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 95 | dataSources: 96 | second: 97 | dbCreate: "update" 98 | url: "jdbc:h2:mem:testDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 99 | production: 100 | dataSource: 101 | dbCreate: "update" 102 | url: "jdbc:h2:prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 103 | properties: 104 | maxActive: -1 105 | minEvictableIdleTimeMillis: 1800000 106 | timeBetweenEvictionRunsMillis: 1800000 107 | numTestsPerEvictionRun: 3 108 | testOnBorrow: true 109 | testWhileIdle: true 110 | testOnReturn: false 111 | validationQuery: "SELECT 1" 112 | jdbcInterceptors: "ConnectionState" 113 | dataSources: 114 | second: 115 | dbCreate: "update" 116 | url: "jdbc:h2:mem:testDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" 117 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/conf/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/conf/spring/resources.groovy: -------------------------------------------------------------------------------- 1 | // Place your Spring DSL code here 2 | beans = { 3 | } 4 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/controllers/UrlMappings.groovy: -------------------------------------------------------------------------------- 1 | class UrlMappings { 2 | 3 | static mappings = { 4 | "/$controller/$action?/$id?(.$format)?"{ 5 | constraints { 6 | // apply constraints here 7 | } 8 | } 9 | 10 | "/"(view:"/index") 11 | "500"(view:'/error') 12 | "404"(view:'/notFound') 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/controllers/test/AuditTrailController.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package test 21 | 22 | import grails.gorm.transactions.Transactional 23 | 24 | @Transactional("second") 25 | class AuditTrailController { 26 | 27 | // the delete, save and update actions only accept POST requests 28 | static allowedMethods = [delete: 'POST', save: 'POST', update: 'POST'] 29 | 30 | def index(Integer max) { 31 | params.max = Math.min(max ?: 10, 100) 32 | respond AuditTrail.list(params), model:[auditTrailCount: AuditTrail.count()] 33 | } 34 | 35 | def show(AuditTrail auditTrail) { 36 | respond auditTrail 37 | } 38 | 39 | def delete() { 40 | redirect(action: 'index') 41 | } 42 | 43 | def edit() { 44 | redirect(action: 'index') 45 | } 46 | 47 | def update() { 48 | redirect(action: 'index') 49 | } 50 | 51 | def create() { 52 | redirect(action: 'index') 53 | } 54 | 55 | def save() { 56 | redirect(action: 'index') 57 | } 58 | 59 | Object search(String query) { 60 | params.max = Math.min(params.max ?: 10, 100) 61 | def auditTrails = AuditTrail.forQuery(query).forDateCreated(params.searchByDate ? params.dateCreated : null).list(params) 62 | render(view: "index", model: [auditTrailList:auditTrails, query:query, byDate:params.searchByDate, auditTrailCount: auditTrails.size()]) 63 | } 64 | } -------------------------------------------------------------------------------- /examples/audit-test/grails-app/controllers/test/AuthorController.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.gorm.transactions.Transactional 4 | 5 | import static org.springframework.http.HttpStatus.* 6 | 7 | @Transactional(readOnly = true) 8 | class AuthorController { 9 | static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"] 10 | 11 | def index(Integer max) { 12 | params.max = Math.min(max ?: 10, 100) 13 | respond Author.list(params), model:[authorCount: Author.count()] 14 | } 15 | 16 | def show(Author author) { 17 | respond author 18 | } 19 | 20 | def create() { 21 | respond new Author(params) 22 | } 23 | 24 | @Transactional 25 | def save(Author author) { 26 | if (author == null) { 27 | transactionStatus.setRollbackOnly() 28 | notFound() 29 | return 30 | } 31 | 32 | if (author.hasErrors()) { 33 | transactionStatus.setRollbackOnly() 34 | respond author.errors, view:'create' 35 | return 36 | } 37 | 38 | author.save flush:true 39 | 40 | request.withFormat { 41 | form multipartForm { 42 | flash.message = message(code: 'default.created.message', args: [message(code: 'author.label', default: 'Author'), author.id]) 43 | redirect author 44 | } 45 | '*' { respond author, [status: CREATED] } 46 | } 47 | } 48 | 49 | def edit(Author author) { 50 | respond author 51 | } 52 | 53 | @Transactional 54 | def update(Author author) { 55 | if (author == null) { 56 | transactionStatus.setRollbackOnly() 57 | notFound() 58 | return 59 | } 60 | 61 | if (author.hasErrors()) { 62 | transactionStatus.setRollbackOnly() 63 | respond author.errors, view:'edit' 64 | return 65 | } 66 | 67 | author.save flush:true 68 | 69 | request.withFormat { 70 | form multipartForm { 71 | flash.message = message(code: 'default.updated.message', args: [message(code: 'author.label', default: 'Author'), author.id]) 72 | redirect author 73 | } 74 | '*'{ respond author, [status: OK] } 75 | } 76 | } 77 | 78 | @Transactional 79 | def delete(Author author) { 80 | 81 | if (author == null) { 82 | transactionStatus.setRollbackOnly() 83 | notFound() 84 | return 85 | } 86 | 87 | author.delete flush:true 88 | 89 | request.withFormat { 90 | form multipartForm { 91 | flash.message = message(code: 'default.deleted.message', args: [message(code: 'author.label', default: 'Author'), author.id]) 92 | redirect action:"index", method:"GET" 93 | } 94 | '*'{ render status: NO_CONTENT } 95 | } 96 | } 97 | 98 | protected void notFound() { 99 | request.withFormat { 100 | form multipartForm { 101 | flash.message = message(code: 'default.not.found.message', args: [message(code: 'author.label', default: 'Author'), params.id]) 102 | redirect action: "index", method: "GET" 103 | } 104 | '*'{ render status: NOT_FOUND } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Aircraft.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class Aircraft implements Auditable { 6 | String type 7 | String description 8 | 9 | static mapping = { 10 | id name: 'type', generator: 'assigned' 11 | } 12 | 13 | static constraints = { 14 | type bindable: true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Airport.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class Airport implements Auditable { 6 | String code 7 | String name 8 | 9 | static hasMany = [runways: Runway] 10 | } 11 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/AuditTrail.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package test 20 | 21 | import groovy.transform.ToString 22 | 23 | /** 24 | * AuditTrails are reported to the AuditLog table. 25 | * This requires you to set up a table or allow 26 | * Grails to create a table for you. (e.g. DDL or db-migration plugin) 27 | */ 28 | @ToString(includes = 'id,className,actor,eventName,propertyName,oldValue,newValue') 29 | class AuditTrail implements Serializable { 30 | private static final long serialVersionUID = 1L 31 | 32 | String id 33 | Date dateCreated 34 | Date lastUpdated 35 | 36 | String actor 37 | String uri 38 | String className 39 | String persistedObjectId 40 | Long persistedObjectVersion = 0 41 | 42 | String eventName 43 | String propertyName 44 | String oldValue 45 | String newValue 46 | 47 | static constraints = { 48 | actor(nullable: true) 49 | uri(nullable: true) 50 | className(nullable: true) 51 | persistedObjectId(nullable: true) 52 | persistedObjectVersion(nullable: true) 53 | eventName(nullable: true) 54 | propertyName(nullable: true) 55 | 56 | oldValue(nullable: true) 57 | newValue(maxSize: 10000, nullable: true) 58 | 59 | // for large column support (as in < 1.0.6 plugin versions), use 60 | // oldValue(nullable: true, maxSize: 65534) 61 | // newValue(nullable: true, maxSize: 65534) 62 | } 63 | 64 | static mapping = { 65 | 66 | table 'audit_log' 67 | 68 | cache usage: 'read-only', include: 'non-lazy' 69 | 70 | // Set similiar when you used "auditLog.useDatasource" in < 1.1.0 plugin version. 71 | // datasource "yourdatasource" 72 | // 73 | // Allow overriding datasource using environment variable 74 | // Used to test different datasources on CI 75 | datasource(System.getProperty("audit-test.AuditTrail.datasource", "second")) 76 | 77 | // no HQL queries package name import (was default in 1.x version) 78 | //autoImport false 79 | 80 | // Since 2.0.0, mapping is not done by config anymore. Configure your ID mapping here. 81 | id generator: "uuid2", type: 'string', length: 36 82 | 83 | version false 84 | } 85 | 86 | static namedQueries = { 87 | forQuery { String q -> 88 | if (!q?.trim()) return // return all 89 | def queries = q.tokenize()?.collect { 90 | '%' + it.replaceAll('_', '\\\\_') + '%' 91 | } 92 | queries.each { query -> 93 | or { 94 | ilike 'actor', query 95 | ilike 'persistedObjectId', query 96 | ilike 'propertyName', query 97 | ilike 'oldValue', query 98 | ilike 'newValue', query 99 | } 100 | } 101 | } 102 | 103 | forDateCreated { Date date -> 104 | if (!date) return 105 | gt 'dateCreated', date 106 | } 107 | } 108 | 109 | /** 110 | * Deserializer that maps a stored map onto the object 111 | * assuming that the keys match attribute properties. 112 | */ 113 | private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { 114 | def map = input.readObject() 115 | map.each { k, v -> this."$k" = v } 116 | } 117 | 118 | /** 119 | * Because Closures do not serialize we can't send the constraints closure 120 | * to the Serialize API so we have to have a custom serializer to allow for 121 | * this object to show up inside a webFlow context. 122 | */ 123 | private void writeObject(ObjectOutputStream out) throws IOException { 124 | def map = [ 125 | id : id, 126 | dateCreated : dateCreated, 127 | lastUpdated : lastUpdated, 128 | 129 | actor : actor, 130 | uri : uri, 131 | className : className, 132 | persistedObjectId : persistedObjectId, 133 | persistedObjectVersion: persistedObjectVersion, 134 | 135 | eventName : eventName, 136 | propertyName : propertyName, 137 | oldValue : oldValue, 138 | newValue : newValue, 139 | ] 140 | out.writeObject(map) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Author.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class Author implements Auditable { 6 | String name 7 | Long age 8 | Boolean famous = false 9 | Publisher publisher 10 | 11 | // This should get masked globally 12 | String ssn = "123-456-7890" 13 | 14 | Date dateCreated 15 | Date lastUpdated 16 | String lastUpdatedBy 17 | 18 | // name, age, famous, publisher, ssn, dateCreated 19 | static int NUMBER_OF_AUDITABLE_PROPERTIES = 6 20 | 21 | static hasMany = [books: Book] 22 | 23 | static constraints = { 24 | lastUpdatedBy nullable: true 25 | publisher nullable: true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Book.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class Book implements Auditable { 6 | String title 7 | String description 8 | Date published 9 | Long pages 10 | 11 | static hasMany = [reviews: Review] 12 | static belongsTo = [author: Author] 13 | 14 | @Override 15 | String getLogEntityId() { 16 | title 17 | } 18 | 19 | static constraints = { 20 | published nullable: true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Coach.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Stampable 4 | 5 | class Coach implements Stampable { 6 | 7 | static constraints = { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/CompositeId.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class CompositeId implements Auditable, Serializable { 6 | 7 | Author author 8 | String string 9 | NonAuditableCompositeId nonAuditableCompositeId 10 | 11 | String notIdString 12 | 13 | static constraints = { 14 | } 15 | 16 | static mapping = { 17 | id composite:['author', 'string', 'nonAuditableCompositeId'] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/EntityInSecondDatastore.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class EntityInSecondDatastore implements Auditable{ 6 | 7 | String name 8 | Integer someIntegerProperty 9 | 10 | static constraints = { 11 | } 12 | 13 | static mapping = { 14 | datasource("second") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Heliport.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class Heliport implements Auditable { 6 | String code 7 | String name 8 | 9 | static mapping = { 10 | version false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/NonAuditableCompositeId.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | class NonAuditableCompositeId implements Serializable { 4 | 5 | String foo 6 | String bar 7 | 8 | @Override 9 | String toString() { 10 | "toString_for_non_auditable_${foo}_${bar}" 11 | } 12 | 13 | static constraints = { 14 | } 15 | 16 | static mapping = { 17 | id composite: ['foo', 'bar'] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Publisher.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.AuditEventType 4 | import grails.plugins.orm.auditable.Auditable 5 | 6 | class Publisher implements Auditable { 7 | String code 8 | String name 9 | 10 | boolean active = false 11 | 12 | @Override 13 | String getLogEntityId() { 14 | "${code}|${name}" 15 | } 16 | 17 | @Override 18 | boolean isAuditable(AuditEventType eventType) { 19 | active 20 | } 21 | 22 | static constraints = { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Resolution.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.AuditEventType 4 | import grails.plugins.orm.auditable.Auditable 5 | 6 | class Resolution implements Auditable { 7 | 8 | String name 9 | 10 | @Override 11 | Collection getLogIgnoreEvents() { 12 | [AuditEventType.UPDATE, AuditEventType.INSERT] 13 | } 14 | 15 | static constraints = { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Review.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class Review implements Auditable { 6 | String name 7 | Book book 8 | 9 | /** 10 | * Override entity id to use a nested entityId from another domain object 11 | * @return 12 | */ 13 | @Override 14 | String getLogEntityId() { 15 | "${name}|${book.logEntityId}" 16 | } 17 | 18 | static constraints = { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Runway.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | class Runway { 4 | Integer length 5 | Integer width 6 | 7 | static belongsTo = [airport: Airport] 8 | 9 | } 10 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/TestEntity.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | @SuppressWarnings("GroovyUnusedDeclaration") 6 | class TestEntity implements Auditable { 7 | String property 8 | String otherProperty 9 | String anotherProperty 10 | 11 | // Just for testing 12 | Serializable ident() { 13 | "id" 14 | } 15 | 16 | @Override 17 | String toString() { 18 | property 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Train.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Stampable 4 | 5 | class Train implements Stampable { 6 | String number 7 | 8 | static constraints = { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Truck.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | 4 | class Truck { 5 | String number 6 | 7 | static constraints = { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/domain/test/Tunnel.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.plugins.orm.auditable.Auditable 4 | 5 | class Tunnel implements Auditable { 6 | String name 7 | String description 8 | 9 | static constraints = { 10 | description maxSize:1000000000, nullable:true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/i18n/messages.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] 2 | default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL 3 | default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number 4 | default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address 5 | default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] 6 | default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}] 7 | default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}] 8 | default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}] 9 | default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}] 10 | default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] 11 | default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation 12 | default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] 13 | default.blank.message=Property [{0}] of class [{1}] cannot be blank 14 | default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] 15 | default.null.message=Property [{0}] of class [{1}] cannot be null 16 | default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique 17 | 18 | default.paginate.prev=Previous 19 | default.paginate.next=Next 20 | default.boolean.true=True 21 | default.boolean.false=False 22 | default.date.format=yyyy-MM-dd HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} created 26 | default.updated.message={0} {1} updated 27 | default.deleted.message={0} {1} deleted 28 | default.not.deleted.message={0} {1} could not be deleted 29 | default.not.found.message={0} not found with id {1} 30 | default.optimistic.locking.failure=Another user has updated this {0} while you were editing 31 | 32 | default.home.label=Home 33 | default.list.label={0} List 34 | default.add.label=Add {0} 35 | default.new.label=New {0} 36 | default.create.label=Create {0} 37 | default.show.label=Show {0} 38 | default.edit.label=Edit {0} 39 | 40 | default.button.create.label=Create 41 | default.button.edit.label=Edit 42 | default.button.update.label=Update 43 | default.button.delete.label=Delete 44 | default.button.delete.confirm.message=Are you sure? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=Property {0} must be a valid URL 48 | typeMismatch.java.net.URI=Property {0} must be a valid URI 49 | typeMismatch.java.util.Date=Property {0} must be a valid Date 50 | typeMismatch.java.lang.Double=Property {0} must be a valid number 51 | typeMismatch.java.lang.Integer=Property {0} must be a valid number 52 | typeMismatch.java.lang.Long=Property {0} must be a valid number 53 | typeMismatch.java.lang.Short=Property {0} must be a valid number 54 | typeMismatch.java.math.BigDecimal=Property {0} must be a valid number 55 | typeMismatch.java.math.BigInteger=Property {0} must be a valid number 56 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/init/audit/test/Application.groovy: -------------------------------------------------------------------------------- 1 | package audit.test 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | 6 | class Application extends GrailsAutoConfiguration { 7 | static void main(String[] args) { 8 | GrailsApp.run(Application, args) 9 | } 10 | } -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/auditTrail/index.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.list.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 | 44 | 45 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/auditTrail/show.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.show.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/author/create.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.create.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 27 | 28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/author/edit.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.edit.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 17 |
18 |

19 | 20 |
${flash.message}
21 |
22 | 23 | 28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/author/index.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.list.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 23 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/author/show.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.show.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 17 |
18 |

19 | 20 |
${flash.message}
21 |
22 | 23 | 24 |
25 | 26 | 27 |
28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/error.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <g:if env="development">Grails Runtime Exception</g:if><g:else>Error</g:else> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |
  • An error has occurred
  • 19 |
  • Exception: ${exception}
  • 20 |
  • Message: ${message}
  • 21 |
  • Path: ${path}
  • 22 |
23 |
24 |
25 | 26 |
    27 |
  • An error has occurred
  • 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/index.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to Grails 6 | 78 | 79 | 80 | 81 | 106 |
107 |

Welcome to Grails

108 |

Congratulations, you have successfully started your first Grails application! At the moment 109 | this is the default page, feel free to modify it to either redirect to a controller or display whatever 110 | content you may choose. Below is a list of controllers that are currently deployed in this application, 111 | click on each to execute its default action:

112 | 113 | 121 |
122 | 123 | 124 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/layouts/main.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:layoutTitle default="Grails"/> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/audit-test/grails-app/views/notFound.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Not Found 5 | 6 | 7 | 8 | 9 |
    10 |
  • Error: Page Not Found (404)
  • 11 |
  • Path: ${request.forwardURI}
  • 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/audit-test/src/integration-test/groovy/test/AuditTransactionSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package test 20 | 21 | import grails.plugins.orm.auditable.AuditLogContext 22 | import grails.testing.mixin.integration.Integration 23 | import groovy.util.logging.Slf4j 24 | import org.hibernate.Session 25 | import org.springframework.transaction.TransactionStatus 26 | import spock.lang.Specification 27 | 28 | @Slf4j 29 | @Integration 30 | class AuditTransactionSpec extends Specification { 31 | 32 | void setup() { 33 | Author.withNewTransaction { 34 | AuditLogContext.withoutAuditLog { 35 | Book.where {}.deleteAll() 36 | Author.where {}.deleteAll() 37 | 38 | def author = new Author(name: "Aaron", age: 37, famous: true) 39 | author.addToBooks(new Book(title: 'Hunger Games', description: 'Blah', pages: 400)) 40 | author.addToBooks(new Book(title: 'Catching Fire', description: 'Blah', pages: 500)) 41 | author.save(flush: true, failOnError: true) 42 | } 43 | } 44 | EntityInSecondDatastore.withNewTransaction { 45 | AuditLogContext.withoutAuditLog { 46 | EntityInSecondDatastore.where {}.deleteAll() 47 | new EntityInSecondDatastore(name: "name", someIntegerProperty: 1).save(flush: true, failOnError: true) 48 | } 49 | } 50 | AuditTrail.withNewTransaction { 51 | AuditTrail.where {}.deleteAll() 52 | assert AuditTrail.count() == 0 53 | } 54 | } 55 | 56 | void "Test rollback behaviour"() { 57 | when: 58 | Author.withNewTransaction { TransactionStatus transactionStatus -> 59 | def author = Author.findByName("Aaron") 60 | author.age = 1 61 | Author.withSession { Session session -> 62 | session.flush() 63 | } 64 | transactionStatus.setRollbackOnly() 65 | } 66 | 67 | then: 68 | Author.withNewTransaction { TransactionStatus transactionStatus -> 69 | Author.findByName("Aaron").age == 37 70 | } 71 | AuditTrail.withNewTransaction { 72 | AuditTrail.list() == [] 73 | } 74 | 75 | when: 76 | Author.withNewTransaction { 77 | def author = Author.findByName("Aaron") 78 | author.age = 3 79 | } 80 | 81 | then: 82 | Author.withNewTransaction { 83 | Author.findByName("Aaron").age == 3 84 | } 85 | AuditTrail.withNewTransaction { 86 | AuditTrail.count 87 | } == 1 88 | } 89 | 90 | void "Test nested transactions"() { 91 | when: 92 | Author.withNewTransaction { TransactionStatus transactionStatus -> 93 | Author.findByName("Aaron").age = 1 94 | Author.withSession { Session session -> 95 | session.flush() 96 | } 97 | Book.withNewTransaction { TransactionStatus transactionStatus2 -> 98 | Book.findByTitle("Hunger Games").pages = 401 99 | } 100 | transactionStatus.setRollbackOnly() 101 | } 102 | 103 | then: 104 | Author.withNewTransaction { 105 | Author.findByName("Aaron") 106 | }.age == 37 // rolled back change -> original value 107 | Book.withNewTransaction { 108 | Book.findByTitle("Hunger Games") 109 | }.pages == 401 // committed change -> new value 110 | AuditTrail.withNewTransaction { 111 | AuditTrail.count 112 | } == 1 // AuditTrail only for committed change 113 | AuditTrail.withNewTransaction { 114 | AuditTrail.list()[0] 115 | }.newValue == "401" 116 | } 117 | 118 | void "test nested transactions different datastores: rollback outer"() { 119 | expect: 120 | AuditTrail.withNewTransaction { 121 | AuditTrail.list().collect { it.className }.unique() 122 | } == [] 123 | 124 | when: 125 | Author.withNewTransaction { TransactionStatus transactionStatus -> 126 | EntityInSecondDatastore.withNewTransaction { 127 | new Author(name: "name2", age: 12, famous: true).save(flush: true, failOnError: true) 128 | new EntityInSecondDatastore(name: "name2", someIntegerProperty: 1).save(flush: true, failOnError: true) 129 | // Commit new EntityInSecondDatastore 130 | } 131 | // Rollback new Author 132 | transactionStatus.setRollbackOnly() 133 | } 134 | 135 | then: 136 | Author.withNewTransaction { 137 | Author.findByName("name2") 138 | } == null 139 | EntityInSecondDatastore.withNewTransaction { 140 | EntityInSecondDatastore.findByName("name2") 141 | } != null 142 | AuditTrail.withNewTransaction { 143 | AuditTrail.list().collect { it.className }.unique() 144 | } == ["test.EntityInSecondDatastore"] 145 | } 146 | 147 | void "test nested transactions different datastores: rollback inner"() { 148 | expect: 149 | AuditTrail.withNewTransaction { 150 | AuditTrail.list().collect { it.className }.unique() 151 | } == [] 152 | 153 | when: 154 | Author.withNewTransaction { 155 | EntityInSecondDatastore.withNewTransaction { TransactionStatus transactionStatus -> 156 | new Author(name: "name2", age: 12, famous: true).save(flush: true, failOnError: true) 157 | new EntityInSecondDatastore(name: "name2", someIntegerProperty: 1).save(flush: true, failOnError: true) 158 | // Rollback new EntityInSecondDatastore 159 | transactionStatus.setRollbackOnly() 160 | } 161 | // Commit new Author 162 | } 163 | 164 | then: 165 | Author.withNewTransaction { 166 | Author.findByName("name2") 167 | } != null 168 | EntityInSecondDatastore.withNewTransaction { 169 | EntityInSecondDatastore.findByName("name2") 170 | } == null 171 | AuditTrail.withNewTransaction { 172 | AuditTrail.list().collect { it.className }.unique() 173 | } == ["test.Author"] 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /examples/audit-test/src/integration-test/groovy/test/AuditTruncateSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package test 20 | 21 | import grails.core.GrailsApplication 22 | import grails.plugins.orm.auditable.AuditLogListener 23 | import grails.plugins.orm.auditable.AuditLoggingConfigUtils 24 | import grails.testing.mixin.integration.Integration 25 | import groovy.util.logging.Slf4j 26 | import org.springframework.beans.factory.annotation.Autowired 27 | import spock.lang.Shared 28 | import spock.lang.Specification 29 | 30 | @Integration 31 | @Slf4j 32 | class AuditTruncateSpec extends Specification { 33 | @Autowired 34 | GrailsApplication grailsApplication 35 | 36 | @Shared 37 | def defaultIgnoreList 38 | 39 | void setup() { 40 | defaultIgnoreList = ['id'] + AuditLoggingConfigUtils.auditConfig.excluded?.asImmutable() ?: [] 41 | AuditTrail.withNewTransaction { 42 | AuditTrail.executeUpdate('delete from AuditTrail') 43 | } 44 | } 45 | 46 | void "No_Truncate"() { 47 | given: 48 | def oldTruncLength = getFirstListenerTruncateLength() 49 | setListenersTruncateLength(255) 50 | 51 | when: 52 | Tunnel.withNewTransaction { 53 | new Tunnel(name: "shortdesc", description: "${'a' * 255}").save(flush: true, failOnError: true) 54 | } 55 | 56 | then: "description is not truncated from auditLog" 57 | def events = AuditTrail.withNewTransaction { 58 | AuditTrail.withCriteria { eq('className', 'test.Tunnel') } 59 | } 60 | events.size() == (Tunnel.gormPersistentEntity.persistentPropertyNames - defaultIgnoreList).size() 61 | 62 | def first = events.find { it.propertyName == 'name' } 63 | first.oldValue == null 64 | first.newValue == "shortdesc" 65 | 66 | def second = events.find { it.propertyName == 'description' } 67 | second.oldValue == null 68 | second.newValue == 'a' * 255 69 | 70 | cleanup: 71 | log.info "Reset truncate length" 72 | setListenersTruncateLength oldTruncLength 73 | } 74 | 75 | void "Truncate_at_255"() { 76 | given: 77 | def oldTruncLength = getFirstListenerTruncateLength() 78 | setListenersTruncateLength(255) 79 | Tunnel.withNewTransaction { 80 | new Tunnel(name: "shortdesc", description: "${'b' * 1024}").save(flush: true, failOnError: true) 81 | } 82 | 83 | expect: "description is truncated at 255 from auditLog" 84 | def events = AuditTrail.withNewTransaction { 85 | AuditTrail.withCriteria { eq('className', 'test.Tunnel') } 86 | } 87 | events.size() == (Tunnel.gormPersistentEntity.persistentPropertyNames - defaultIgnoreList).size() 88 | 89 | def first = events.find { it.propertyName == 'name' } 90 | first.oldValue == null 91 | first.newValue == "shortdesc" 92 | 93 | def second = events.find { it.propertyName == 'description' } 94 | log.info second.newValue 95 | second.oldValue == null 96 | second.newValue == 'b' * 255 97 | 98 | cleanup: 99 | setListenersTruncateLength oldTruncLength 100 | } 101 | 102 | void "Truncate_at_1024"() { 103 | given: 104 | def oldTruncLength = getFirstListenerTruncateLength() 105 | setListenersTruncateLength(1024) 106 | Tunnel.withNewTransaction { 107 | new Tunnel(name: "shortdesc", description: "${'b' * 4096}").save(flush: true, failOnError: true) 108 | } 109 | 110 | expect: "description is truncated at 255 from auditLog" 111 | def events = AuditTrail.withNewTransaction { 112 | AuditTrail.withCriteria { eq('className', 'test.Tunnel') } 113 | } 114 | events.size() == (Tunnel.gormPersistentEntity.persistentPropertyNames - defaultIgnoreList).size() 115 | 116 | def first = events.find { it.propertyName == 'name' } 117 | first.oldValue == null 118 | first.newValue == "shortdesc" 119 | 120 | def second = events.find { it.propertyName == 'description' } 121 | log.debug second.newValue 122 | second.oldValue == null 123 | second.newValue == 'b' * 1024 124 | 125 | cleanup: 126 | log.debug "Reset truncate length" 127 | setListenersTruncateLength oldTruncLength 128 | } 129 | 130 | private int getFirstListenerTruncateLength() { 131 | List auditListeners = grailsApplication.parentContext.applicationEventMulticaster.applicationListeners.findAll { it.class.simpleName == "AuditLogListener" } 132 | auditListeners?.first()?.truncateLength 133 | } 134 | 135 | private void setListenersTruncateLength(int truncateLength) { 136 | log.info("Set all AuditLogListeners truncate length ${truncateLength}") 137 | // Change the truncateLength of all auditLogListeners - Never do that in your application! 138 | List auditListeners = grailsApplication.parentContext.applicationEventMulticaster.applicationListeners.findAll { it.class.simpleName == "AuditLogListener" } 139 | auditListeners*.truncateLength = truncateLength 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /examples/audit-test/src/integration-test/groovy/test/StampSpec.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import grails.core.GrailsApplication 4 | import grails.gorm.transactions.Rollback 5 | import grails.plugins.orm.auditable.resolvers.DefaultAuditRequestResolver 6 | import grails.spring.BeanBuilder 7 | import grails.testing.mixin.integration.Integration 8 | import org.springframework.test.annotation.DirtiesContext 9 | import spock.lang.Specification 10 | 11 | @Integration 12 | @Rollback 13 | class StampSpec extends Specification { 14 | GrailsApplication grailsApplication 15 | 16 | @DirtiesContext 17 | void 'Stamp inserted with custom request resolver'() { 18 | given: 19 | def train = new Train(number: "10") 20 | 21 | and: "custom request resolver" 22 | BeanBuilder bb = new BeanBuilder() 23 | bb.beans { 24 | auditRequestResolver(CustomRequestResolver) 25 | } 26 | bb.registerBeans(grailsApplication.mainContext) 27 | 28 | when: 29 | train.save(flush: true, failOnError: true) 30 | 31 | then: 32 | train.createdBy == 'Aaron' 33 | train.lastUpdatedBy == 'Aaron' 34 | train.dateCreated 35 | train.lastUpdated 36 | 37 | when: 38 | train.number = "20" 39 | train.save(flush: true) 40 | 41 | then: 42 | train.dateCreated != train.lastUpdated 43 | } 44 | 45 | void 'Stamp inserted with default request resolver'() { 46 | given: 47 | def train = new Train(number: "10") 48 | 49 | when: 50 | train.save(flush: true) 51 | 52 | then: 53 | train.createdBy == 'SYS' 54 | train.lastUpdatedBy == 'SYS' 55 | train.dateCreated 56 | train.lastUpdated 57 | 58 | when: 59 | train.number = "20" 60 | train.save(flush: true) 61 | 62 | then: 63 | train.dateCreated != train.lastUpdated 64 | } 65 | } 66 | 67 | class CustomRequestResolver extends DefaultAuditRequestResolver { 68 | @Override 69 | String getCurrentActor() { 70 | "Aaron" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/audit-test/src/main/groovy/test/TestUtils.groovy: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import org.grails.datastore.mapping.model.PersistentEntity 4 | import org.grails.datastore.mapping.model.PersistentProperty 5 | 6 | class TestUtils { 7 | static getAuditableProperties(PersistentEntity entity, List ignoreList) { 8 | List properties = entity.persistentProperties.findResults { PersistentProperty p -> 9 | return p.name 10 | } 11 | return properties - ignoreList 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | projectVersion=6.0.0-SNAPSHOT 2 | 3 | asciidoctorJvmVersion=4.0.3 4 | customUserDataVersion=2.2.1 5 | develocityVersion=4.0 6 | grailsVersion=7.0.0-SNAPSHOT 7 | 8 | #For tests only 9 | hibernate5Version=5.6.15.Final 10 | jbossTransactionApiVersion=2.0.0.Final 11 | 12 | org.gradle.daemon=true 13 | org.gradle.parallel=true 14 | org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M 15 | -------------------------------------------------------------------------------- /gradle/docs.gradle: -------------------------------------------------------------------------------- 1 | import java.text.SimpleDateFormat 2 | 3 | static String buildDate() { 4 | def df = new SimpleDateFormat("yyyy-MM-dd") 5 | df.setTimeZone(TimeZone.getTimeZone("UTC")) 6 | return df.format(new Date()) 7 | } 8 | 9 | asciidoctor { 10 | logDocuments true 11 | baseDirFollowsSourceDir() 12 | 13 | sourceDir = file('src/docs') 14 | sources { 15 | include 'index.adoc' 16 | } 17 | 18 | outputDir new File(buildDir, 'docs') 19 | 20 | outputOptions { 21 | backends = ['html5', 'pdf', 'epub3'] 22 | separateOutputDirs = false 23 | } 24 | 25 | attributes 'experimental': 'true', 26 | 'source-highlighter': 'coderay', 27 | 'compat-mode': 'true', 28 | toc: 'left', 29 | icons: 'font', 30 | setanchors: 'true', 31 | idprefix: '', 32 | idseparator: '-', 33 | toc2: '', 34 | numbered: '', 35 | version: project.version, 36 | groupId: project.group, 37 | artifactId: project.name, 38 | revnumber: project.version, 39 | revdate: buildDate() 40 | } 41 | 42 | task docs(dependsOn: 'asciidoctor') { 43 | doLast { 44 | File dir = new File(buildDir, 'docs') 45 | dir.mkdirs() 46 | 47 | ['pdf', 'epub'].each { String ext -> 48 | File f = new File(dir, 'index.' + ext) 49 | if (f.exists()) { 50 | f.renameTo new File(dir, project.name + '-' + project.version + '.' + ext) 51 | } 52 | } 53 | 54 | new File(buildDir, 'docs/ghpages.html') << file('src/docs/templates/index.tmpl').text.replaceAll("@VERSION@", project.version).replaceAll("@DOCDATE@", buildDate()) 55 | 56 | copy { 57 | from 'src/docs' 58 | into new File(buildDir, 'docs').path 59 | include '**/*.png' 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "org.asciidoctor.jvm.convert" version "$asciidoctorJvmVersion" 3 | id "org.asciidoctor.jvm.pdf" version "$asciidoctorJvmVersion" 4 | id "org.asciidoctor.jvm.epub" version "$asciidoctorJvmVersion" 5 | } 6 | 7 | version = project.projectVersion 8 | group = "org.grails.plugins" 9 | 10 | apply plugin: 'eclipse' 11 | apply plugin: 'idea' 12 | apply plugin: 'org.apache.grails.gradle.grails-plugin' 13 | apply plugin: 'org.apache.grails.gradle.grails-gsp' 14 | 15 | compileJava.options.release = 17 16 | 17 | dependencies { 18 | implementation platform("org.apache.grails:grails-bom:$grailsVersion") 19 | 20 | profile "org.apache.grails.profiles:web-plugin" 21 | compileOnly 'org.springframework.boot:spring-boot-starter-logging' 22 | compileOnly "org.springframework.boot:spring-boot-starter-actuator" 23 | compileOnly "org.springframework.boot:spring-boot-autoconfigure" 24 | compileOnly "org.springframework.boot:spring-boot-starter-tomcat" 25 | compileOnly "org.apache.grails:grails-web-boot" 26 | compileOnly "org.apache.grails:grails-core" 27 | compileOnly "org.apache.grails:grails-logging" 28 | compileOnly "org.apache.grails:grails-rest-transforms" 29 | compileOnly "org.apache.grails:grails-i18n" 30 | compileOnly "org.apache.grails:grails-services" 31 | compileOnly "org.apache.grails:grails-url-mappings" 32 | compileOnly "org.apache.grails:grails-interceptors" 33 | compileOnly "org.apache.grails:grails-gsp" 34 | compileOnly "org.apache.grails:grails-console" 35 | compileOnly "org.apache.grails:grails-services" 36 | compileOnly "org.apache.grails:grails-domain-class" 37 | 38 | // TODO: We need to make this configurable/pluggable 39 | compileOnly "org.apache.grails:grails-data-hibernate5" 40 | 41 | testCompileOnly "org.apache.grails:grails-testing-support-web" 42 | } 43 | 44 | bootJar.enabled = false 45 | 46 | apply from: "${rootProject.projectDir}/gradle/docs.gradle" -------------------------------------------------------------------------------- /plugin/grails-app/conf/DefaultAuditLogConfig.groovy: -------------------------------------------------------------------------------- 1 | /* Copyright 2006-2015 the original author or authors. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | // Default Plugin configuration 17 | defaultAuditLog { 18 | auditDomainClassName = null 19 | 20 | disabled = false 21 | verbose = true 22 | failOnError = false 23 | logIds = true 24 | logFullClassName = true 25 | excluded = ['version', 'lastUpdated', 'lastUpdatedBy'] 26 | included = null 27 | mask = ['password'] 28 | propertyMask = "**********" 29 | defaultActor = 'SYS' 30 | usePersistentDirtyPropertyValues = true 31 | 32 | // Enable support for Stampable 33 | stampEnabled = true 34 | } 35 | -------------------------------------------------------------------------------- /plugin/grails-app/conf/application.yml: -------------------------------------------------------------------------------- 1 | grails: 2 | profile: web-plugin 3 | codegen: 4 | defaultPackage: audit.logging 5 | mime: 6 | disable: 7 | accept: 8 | header: 9 | userAgents: 10 | - Gecko 11 | - WebKit 12 | - Presto 13 | - Trident 14 | types: 15 | all: '*/*' 16 | atom: application/atom+xml 17 | css: text/css 18 | csv: text/csv 19 | form: application/x-www-form-urlencoded 20 | html: 21 | - text/html 22 | - application/xhtml+xml 23 | js: text/javascript 24 | json: 25 | - application/json 26 | - text/json 27 | multipartForm: multipart/form-data 28 | rss: application/rss+xml 29 | text: text/plain 30 | hal: 31 | - application/hal+json 32 | - application/hal+xml 33 | xml: 34 | - text/xml 35 | - application/xml 36 | urlmapping: 37 | cache: 38 | maxsize: 1000 39 | controllers: 40 | defaultScope: singleton 41 | converters: 42 | encoding: UTF-8 43 | views: 44 | default: 45 | codec: html 46 | gsp: 47 | encoding: UTF-8 48 | htmlcodec: xml 49 | codecs: 50 | expression: html 51 | scriptlets: html 52 | taglib: none 53 | staticparts: none 54 | 55 | info: 56 | app: 57 | name: '@info.app.name@' 58 | version: '@info.app.version@' 59 | grailsVersion: '@info.app.grailsVersion@' 60 | 61 | spring: 62 | groovy: 63 | template: 64 | check-template-location: false 65 | 66 | 67 | hibernate: 68 | cache: 69 | queries: false 70 | 71 | 72 | dataSource: 73 | pooled: true 74 | jmxExport: true 75 | driverClassName: org.h2.Driver 76 | username: sa 77 | password: 78 | 79 | environments: 80 | development: 81 | dataSource: 82 | dbCreate: create-drop 83 | url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 84 | test: 85 | dataSource: 86 | dbCreate: update 87 | url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 88 | production: 89 | dataSource: 90 | dbCreate: update 91 | url: jdbc:h2:prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 92 | properties: 93 | jmxEnabled: true 94 | initialSize: 5 95 | maxActive: 50 96 | minIdle: 5 97 | maxIdle: 25 98 | maxWait: 10000 99 | maxAge: 600000 100 | timeBetweenEvictionRunsMillis: 5000 101 | minEvictableIdleTimeMillis: 60000 102 | validationQuery: SELECT 1 103 | validationQueryTimeout: 3 104 | validationInterval: 15000 105 | testOnBorrow: true 106 | testWhileIdle: true 107 | testOnReturn: false 108 | jdbcInterceptors: ConnectionState 109 | defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED 110 | -------------------------------------------------------------------------------- /plugin/grails-app/conf/logback.groovy: -------------------------------------------------------------------------------- 1 | import grails.util.BuildSettings 2 | import grails.util.Environment 3 | 4 | 5 | // See http://logback.qos.ch/manual/groovy.html for details on configuration 6 | appender('STDOUT', ConsoleAppender) { 7 | encoder(PatternLayoutEncoder) { 8 | pattern = "%level %logger - %msg%n" 9 | } 10 | } 11 | 12 | root(ERROR, ['STDOUT']) 13 | 14 | if(Environment.current == Environment.DEVELOPMENT) { 15 | def targetDir = BuildSettings.TARGET_DIR 16 | if(targetDir) { 17 | 18 | appender("FULL_STACKTRACE", FileAppender) { 19 | 20 | file = "${targetDir}/stacktrace.log" 21 | append = true 22 | encoder(PatternLayoutEncoder) { 23 | pattern = "%level %logger - %msg%n" 24 | } 25 | } 26 | logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /plugin/grails-app/controllers/grails/plugins/orm/auditable/AuditLogEventController.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package grails.plugins.orm.auditable 20 | 21 | class AuditLogEventController { 22 | protected Class AuditLogEvent = AuditLogListenerUtil.auditDomainClass 23 | 24 | // the delete, save and update actions only accept POST requests 25 | static allowedMethods = [delete: 'POST', save: 'POST', update: 'POST'] 26 | 27 | def index() { 28 | redirect(action: 'list', params: params) 29 | } 30 | 31 | def list() { 32 | if (!params.max) { 33 | params.max = 10 34 | } 35 | 36 | [auditLogEventInstanceList: AuditLogEvent.list(params), auditLogEventInstanceTotal: AuditLogEvent.count()] 37 | } 38 | 39 | def show() { 40 | def auditLogEvent = AuditLogEvent.get(params.id) 41 | if (auditLogEvent == null) { 42 | flash.message = "AuditLogEvent not found with id ${params.id}" 43 | redirect(action: 'list') 44 | return 45 | } 46 | [auditLogEventInstance: auditLogEvent] 47 | } 48 | 49 | def delete() { 50 | redirect(action: 'list') 51 | } 52 | 53 | def edit() { 54 | redirect(action: 'list') 55 | } 56 | 57 | def update() { 58 | redirect(action: 'list') 59 | } 60 | 61 | def create() { 62 | redirect(action: 'list') 63 | } 64 | 65 | def save() { 66 | redirect(action: 'list') 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /plugin/grails-app/init/grails/plugins/orm/auditable/Application.groovy: -------------------------------------------------------------------------------- 1 | package audit.logging 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | 6 | class Application extends GrailsAutoConfiguration { 7 | static void main(String[] args) { 8 | GrailsApp.run(Application, args) 9 | } 10 | } -------------------------------------------------------------------------------- /plugin/grails-app/views/auditLogEvent/list.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AuditLogEvent List 6 | 7 | 8 | 9 | 12 | 13 |
14 |

AuditLogEvent List

15 | 16 |
${flash.message}
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
${fieldValue(bean: auditLogEventInstance, field: 'id')}${fieldValue(bean: auditLogEventInstance, field: 'actor')}${fieldValue(bean: auditLogEventInstance, field: 'uri')}${fieldValue(bean: auditLogEventInstance, field: 'className')}${fieldValue(bean: auditLogEventInstance, field: 'persistedObjectId')}${fieldValue(bean: auditLogEventInstance, field: 'persistedObjectVersion')}${fieldValue(bean: auditLogEventInstance, field: 'eventName')}${fieldValue(bean: auditLogEventInstance, field: 'propertyName')}${fieldValue(bean: auditLogEventInstance, field: 'oldValue')}${fieldValue(bean: auditLogEventInstance, field: 'newValue')}
51 |
52 | 53 |
54 | 55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /plugin/grails-app/views/auditLogEvent/show.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Show AuditLogEvent 8 | 9 | 10 | 14 |
15 |

Show AuditLogEvent

16 | 17 |
${flash.message}
18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
Id:${fieldValue(bean:auditLogEventInstance, field:'id')}
Actor:${fieldValue(bean:auditLogEventInstance, field:'actor')}
Uri:${fieldValue(bean:auditLogEventInstance, field:'uri')}
Class Name:${fieldValue(bean:auditLogEventInstance, field:'className')}
Persisted Object Id:${fieldValue(bean:auditLogEventInstance, field:'persistedObjectId')}
Persisted Object Version:${fieldValue(bean:auditLogEventInstance, field:'persistedObjectVersion')}
Event Name:${fieldValue(bean:auditLogEventInstance, field:'eventName')}
Property Name:${fieldValue(bean:auditLogEventInstance, field:'propertyName')}
Old Value:${fieldValue(bean:auditLogEventInstance, field:'oldValue')}
New Value:${fieldValue(bean:auditLogEventInstance, field:'newValue')}
Date Created:${fieldValue(bean:auditLogEventInstance, field:'dateCreated')}
Last Updated:${fieldValue(bean:auditLogEventInstance, field:'lastUpdated')}
110 |
111 |
112 | 113 | 114 | -------------------------------------------------------------------------------- /plugin/src/docs/changelog.adoc: -------------------------------------------------------------------------------- 1 | == Change Log 2 | * 6.0.0 3 | ** Updated for Grails 7 4 | 5 | * 5.0.0 6 | ** New: Participate in current Transaction by queuing audit logs. Supports nested transactions. Plugin is currently NOT ORM agnostic and needs Hibernate due to this change (PR #212). See documentation. 7 | ** Fix #213 nested withoutAuditLog calls 8 | ** Fix #192 Column 'date_created' cannot be null (PR #218) 9 | ** Fix #175 javax.persistence.TransactionRequiredException: no transaction is in progress 10 | ** Fix #173 Changes to withNewSession semantics make it useless for audit log listener 11 | 12 | * 4.0.2 13 | ** Fix #210 Add support for non-Date dateCreated/lastUpdated types 14 | ** Fix #203 Prevent exception if AuditRequestResolverBean is not initialized (e.g. in tests) 15 | 16 | * 4.0.1 17 | ** Fix #189 IllegalArgumentException in validator 18 | 19 | * 4.0.0 20 | ** Initial support for Grails 4 21 | 22 | * 3.0.5 23 | ** Add support for getOriginalValue() using config parameter. 24 | ** Fix #193 StackOverflow with composite ID and Auditable 25 | 26 | * 3.0.4 27 | ** Fix #153 NPE on delete for Hibernate PersistentCollection associations 28 | ** Fix documentation for verboseEvents 29 | 30 | * 3.0.3 31 | ** Fix #180, Fixed checking of the pull request in the travis-build.sh 32 | ** Fix #176, #178, #179 Mongo datastores don't have a dataSourceName property 33 | 34 | * 3.0.2 35 | ** Fix #174 Merge default config before application ctx is refreshed 36 | 37 | * 3.0.1 38 | ** Fix #169 Crash if project is not using Spring-Security-Core plugin 39 | ** Minor changes to EventLogController 40 | 41 | * 3.0.0 42 | ** Major rewrite of plugin to be trait based 43 | ** Removed support for handler callbacks 44 | ** Consolidated and cleaned up configuration 45 | ** Added AuditLogContext to allow configuration overrides at the block level 46 | 47 | * 2.0.6 48 | ** Fix #142 Re-introduced truncateLength support (and changed config parameter from 'TRUNCATE_LENGTH' to 'truncateLength') 49 | ** Fixed verbose param default description in documentation (Thanks to Matthew Moss) 50 | ** Fix #139 Allow whitelist of properties to be used instead of a ignore list. 51 | 52 | * 2.0.5 53 | ** Only pass session to actorClosure if a session actually exists. (Thanks to Dennie de Lange) 54 | ** Updated syncHibernateState to use correct name array (Thanks to Matthias Born) 55 | ** Fix ignore list not used for insert and delete (Backport from 1.x) 56 | ** Fix #147 Document per-datasource auditLog.disabled config key 57 | 58 | * 2.0.4 59 | ** Added option to specify createdBy,lastUpdatedBy, dateCreated,lastUpdated fieldnames per domainclass 60 | ** and removed blank constraint for nullable stampable properties. 61 | ** Remove preDelete as stampable event, does not make sense to stamp a delete event. (Thanks to Dennie de Lange) 62 | ** Constraint fixes 63 | 64 | * 2.0.3 65 | ** Fix #129 Issue with Hibernate stamping. Stamping was ignored with dynamicUpdate = true and stamping was ignored on cascading saves. (thanks to Dennie de Lange) 66 | ** Fix #130 Docs for verbose mode 67 | 68 | * 2.0.2 69 | ** Fix #118, use Grails 3.0.10 internally. 70 | ** Fix #126 Support Many-To-Many logging (thanks to Andrey Zhuchkov) 71 | 72 | * 2.0.1 73 | ** Fix #117 Clean build. Version 2.0.0 had issues with Spring Security due to unclean build. 74 | ** Fix #116 (partially). Replacement Patterns do work, but trailing dots are ignored for now due to Grails 3.0.x limitations. 75 | 76 | * 2.0.0 77 | ** First Grails 3 version. Thanks to Graeme Rocher. 78 | ** Added audit-quickstart command to create the AuditLog domain artifact 79 | ** #96 Make identifiers available in the maps during onChange event. Thanks to dmahapatro. 80 | ** branch: master. 81 | ** For 1.0.x plugin version (Grails2), see 1.x_maintenance branch 82 | 83 | * 1.0.5 84 | ** Migration of JIRA to GitHub Issues 85 | ** Fix #92 (Support for ignoring certain Events) 86 | ** Starting with this release, the main branch for the 1.0.x series is 1.x_maintenance. Master branch is for Grails 3.0 support, now. Both branches will be tested by Travis-CI. 87 | 88 | * 1.0.4 89 | ** GPAUDITLOGGING-69 allow to set uri per domain object 90 | ** GPAUDITLOGGING-62 Add identifier in handler map 91 | ** GPAUDITLOGGING-29 support configurable id mapping for AuditLogEvent 92 | ** GPAUDITLOGGING-70 support configurable datasource name for AuditLogEvent 93 | ** GPAUDITLOGGING-74 Impossible to log values of zero or false 94 | ** GPAUDITLOGGING-75 Support automatic (audit) stamping support on entities 95 | 96 | * 1.0.3 97 | 98 | ** GPAUDITLOGGING-64 workaround for duplicate log entries written per configured dataSource 99 | ** GPAUDITLOGGING-63 logFullClassName property 100 | 101 | * 1.0.2 102 | ** GPAUDITLOGGING-66 103 | 104 | 105 | * 1.0.1 106 | ** closures 107 | ** nonVerboseDelete property 108 | ** provide domain identifier to onSave() handler 109 | 110 | * 1.0.0 111 | ** Grails >= 2.0 112 | ** ORM agnostic implementation 113 | ** major cleanup and new features 114 | ** fix #99 Plugin not working with MongoDB as Only Database 115 | ** Changed issue management url to GH. 116 | ** #13 Externalize AuditTrailEvent domain to user 117 | 118 | 119 | * 0.5.5.3 120 | ** Added ability to disable audit logging by config. 121 | 122 | 123 | * 0.5.5.2 124 | ** Added issueManagement to plugin descriptor for the portal. No changes in the plugin code. 125 | 126 | * 0.5.5.1 127 | ** Fixed the title. No changes in the plugin code. 128 | 129 | * 0.5.5 130 | ** collections logging 131 | ** log ids 132 | ** replacement patterns 133 | ** property value masking 134 | ** large fields support 135 | ** fixes and enhancements 136 | 137 | * 0.5.4 138 | ** compatibility issues with Grails 1.3.x 139 | 140 | * 0.5.3 141 | ** GRAILSPLUGINS-2135 142 | ** GRAILSPLUGINS-2060 143 | ** an issue with extra JAR files that are somehow getting released as part of the plugin 144 | 145 | * 0.5.2 146 | ** GRAILSPLUGINS-1887 and GRAILSPLUGINS-1354 147 | 148 | * 0.5.1 149 | ** fixes regression in field logging 150 | 151 | * 0.5 152 | ** GRAILSPLUGINS-391 153 | ** GRAILSPLUGINS-1496 154 | ** GRAILSPLUGINS-1181 155 | ** GRAILSPLUGINS-1515 156 | ** GRAILSPLUGINS-1811 157 | ** changes to AuditLogEvent domain object uses composite id to simplify logging 158 | ** changes to AuditLogListener uses new domain model with separate transaction 159 | ** for logging action to avoid invalidating the main hibernate session. 160 | 161 | * 0.4.1 162 | ** repackaged for Grails 1.1.1 see GRAILSPLUGINS-1181 163 | 164 | * 0.4 165 | ** custom serializable implementation for AuditLogEvent so events can happen inside a webflow context. 166 | ** tweak application.properties for loading in other grails versions 167 | ** update to views to show URI in an event 168 | ** fix missing oldState bug in change event 169 | 170 | * 0.3 171 | ** actorKey and username features allow for the logging of user or userPrincipal for most security systems. 172 | ** Fix #31 disable hotkeys for layout. 173 | -------------------------------------------------------------------------------- /plugin/src/docs/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grails-plugins/grails-audit-logging-plugin/5b1f6730dd71134467d9f99f0cc137fc92f460b6/plugin/src/docs/images/cover.png -------------------------------------------------------------------------------- /plugin/src/docs/implementation.adoc: -------------------------------------------------------------------------------- 1 | == Implementation 2 | Most of the plugin code is marked as @CompileStatic. 3 | 4 | == Auditing and Transactions 5 | All audit events for the current transaction are queued up. 6 | They are flushed after the transaction has successfully been committed. 7 | This allows the AuditLogListener to use its own session and transaction. 8 | 9 | === AuditLogEventListener 10 | The Audit Logging plugin registers a PersistenceEventListener (`AuditLogListener`) bean per datasource, which listens to GORM events. 11 | 12 | === StampEventListener 13 | The plugin registers a separate StampEventListener that responds to validate events. Ideally, we would stamp when an insert or update event occurs, but since the attributes are non-null by default, we need to populate them before the default validation is triggered. 14 | 15 | === Plugin Descriptor 16 | The Plugin Descriptor (AuditLogListenerGrailsPlugin) configures the plugin during startup. 17 | 18 | * Configures the plugin either by default values - see DefaultAuditLogConfig.groovy - or by user configured settings. 19 | * Registers a PersistenceEventListener bean per datasource 20 | 21 | === Auditable trait 22 | Enabling auditing on a Domain class is done by implementing the `Auditable` trait. 23 | 24 | === Stampable trait 25 | Enabling stamping on a Domain class is done by implementing the `Stampable` trait. 26 | This trait adds the properties dateCreated, lastUpdated, createdBy and lastUpdatedBy 27 | to the domain class. -------------------------------------------------------------------------------- /plugin/src/docs/index.adoc: -------------------------------------------------------------------------------- 1 | = Grails Audit Logging Plugin 2 | Robert Oschwald 3 | :doctype: book 4 | :encoding: utf-8 5 | :lang: en 6 | :toc: left 7 | :toclevels: 2 8 | :numbered: 9 | :sectanchors: 10 | :keywords: Grails, AuditLog, AuditTrail 11 | ifdef::backend-epub3[] 12 | :front-cover-image: image:cover.png[Front Cover,800,600] 13 | :producer: Asciidoctor 14 | :copyright: Apache License 2.0 15 | :imagesdir: images 16 | endif::[] 17 | 18 | Other formats: link:audit-logging-{VERSION}.pdf[PDF], link:audit-logging-{VERSION}.epub[EPUB] 19 | 20 | include::introduction.adoc[] 21 | 22 | include::changelog.adoc[] 23 | 24 | include::installation.adoc[] 25 | 26 | include::upgrading.adoc[] 27 | 28 | include::configuration.adoc[] 29 | 30 | include::usage.adoc[] 31 | 32 | include::stamping.adoc[] 33 | 34 | include::implementation.adoc[] 35 | -------------------------------------------------------------------------------- /plugin/src/docs/installation.adoc: -------------------------------------------------------------------------------- 1 | == Installation 2 | === Dependencies 3 | Add to your build.gradle project dependencies block: 4 | 5 | WARNING: Due to the move to Maven Central, the plugin groupId has changed from org.grails.plugin to com.symentis. 6 | 7 | [source,groovy] 8 | ---- 9 | dependencies { 10 | compile 'com.symentis:audit-logging:{VERSION}' 11 | } 12 | ---- 13 | Then run the following to refresh gradle dependencies: 14 | 15 | [source,gradle] 16 | ---- 17 | gradle classes 18 | ---- 19 | 20 | NOTE: After installing the plugin, you must perform the following command to let the plugin create the audit-logging domain class and register it in application.groovy within your project. 21 | 22 | === Create Audit Domain Artifact 23 | 24 | grails audit-quickstart 25 | 26 | For example: 27 | 28 | grails audit-quickstart org.myaudit.example AuditTrail 29 | 30 | This will create the Audit Domain class and adds to application.groovy: 31 | 32 | grails.plugin.auditLog.auditDomainClassName = 'org.myaudit.example.AuditTrail' 33 | 34 | Once created, you should open the generated file to adjust the `mappings` and `constraints` to suit your needs. Next, setup additional plugin config. See 35 | 36 | WARNING: Be sure to respect the existing nullability constraints or the plugin may not work correctly. You must update your db schema either by DDL, or using the database-migration plugin (recommended) 37 | 38 | NOTE: Since version 4.0.2 the dateCreated / lastUpdated fields can be of any type which is supported by the Grails DefaultTimestampProvider. By default, they are defined as java.util.Date in the created audit domain artifact. Both fields must be of same type. 39 | 40 | === Prepare your Domain classes for Auditing 41 | 42 | For every Domain class you want to be audited, implement the Auditable Trait. 43 | 44 | For example: 45 | 46 | ```groovy 47 | import grails.plugins.orm.auditable.Auditable 48 | 49 | class MyDomain implements Auditable { 50 | String whatever 51 | ... 52 | } 53 | ``` 54 | 55 | 56 | If you additionally want to enable stamping, implement the Stampable Trait: 57 | 58 | ```groovy 59 | import grails.plugins.orm.auditable.Auditable 60 | 61 | class MyDomain implements Auditable, Stampable { 62 | String whatever 63 | ... 64 | } 65 | ``` -------------------------------------------------------------------------------- /plugin/src/docs/introduction.adoc: -------------------------------------------------------------------------------- 1 | == Description 2 | 3 | The Audit Logging plugin makes it easy to add customizable audit tracking by simply having your domain classes extend the `Auditable` trait. 4 | 5 | It also provides a convenient way to add Update and Create user stamping by extending the `Stampable` trait. 6 | 7 | === Compatibility 8 | 9 | [cols="1,2",width="50%",options="header,footer"] 10 | |==================== 11 | |Grails Version | Audit Logging Plugin Version 12 | |4.0.0 and later | 5.0.0 (participating in Transactions). This is the recommended version if you use Grails 4. 13 | |4.0.x | 4.0.x 14 | |3.3.x | 3.0.x 15 | |3.0.0 to 3.2.0 | 2.0.x 16 | |2.0.0 to 2.5 | 1.0.x 17 | |1.3.x | 0.5.5.3 18 | |< 1.2.0 | 0.5.3 19 | |==================== 20 | 21 | === GORM Compatibility 22 | 23 | This plugin is GORM agnostic, so you can use it with the GORM implementation of your choice 24 | (Hibernate 4 or 5, MongoDB, etc.). 25 | 26 | Please note, that only Hibernate5 is tested during development. If an issue occurs with your ORM mapper, 27 | please file a GitHub issue. 28 | 29 | While the plugin can log transactions for a MongoDB DataSource, it currently does not support MongoDB as an audit logging backend. 30 | If you use this plugin to log MongoDB transactions, you must configure a SQL DataSource for Audit Logging. See Ticket #181. 31 | -------------------------------------------------------------------------------- /plugin/src/docs/stamping.adoc: -------------------------------------------------------------------------------- 1 | == Stamping 2 | Stamping adds the following attributes to a domain object via the `Stampable` trait: 3 | 4 | ```groovy 5 | trait Stampable extends GormEntity { 6 | Date dateCreated 7 | Date lastUpdated 8 | 9 | String createdBy 10 | String lastUpdatedBy 11 | } 12 | ``` 13 | 14 | The attributes will assume the default constraints, which should be fine for most cases. 15 | 16 | You can configure the `constraints` and `mappings` block as usual in your domain class to customize the stampable properties. 17 | 18 | === Stamping Configuration 19 | Previous versions used an AST transformation to apply the stamping and could allow more configuration. 20 | For now, the stamping support has been boiled down to just the basic trait attributes and the ability to disable the plugin globally. 21 | 22 | ```groovy 23 | // Enable or disable stamping 24 | grails.plugin.auditLog.stampEnabled = true 25 | ``` 26 | 27 | WARNING: If you disable stamping but have `Stampable` entities, they will likely fail validation since the plugin will not be populating the fields which are still added to the domain objects. The main reason to disable stamping is to prevent the listener registration in the case that you just aren't using the `Stampable` support. 28 | 29 | If you want to mark *all* of your domain objects as stampable, you could define the following `TraitInjector`: 30 | 31 | ```groovy 32 | @CompileStatic 33 | class StampableTraitInjector implements TraitInjector { 34 | @Override 35 | Class getTrait() { 36 | Stampable 37 | } 38 | 39 | @Override 40 | String[] getArtefactTypes() { 41 | ['Domain'] as String[] 42 | } 43 | } 44 | ``` -------------------------------------------------------------------------------- /plugin/src/docs/templates/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Grails Audit-Logging Plugin 8 | 9 | 58 | 59 | 60 | 61 | 62 | 63 | Fork me on GitHub 64 | 65 | 66 |
67 |

Grails Audit Logging Plugin

68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
Version@VERSION@ 73 |
Date@DOCDATE@ 77 |
Grails Version3.3.0 > *
AuthorRobert Oschwald
88 | 89 |
90 | 91 |

Current 3.x Documentation (Grails 3.3.x)

92 | 95 | 96 |

Snapshot Documentation (Grails 3.3.x)

97 | 100 | 101 |

2.x Documentation (Grails 3.0.x - 3.2.x)

102 | 105 | 106 |

1.x Documentation for Grails 1.x / 2.x

107 | 110 | 111 |
112 | 113 | 121 | 122 |

Download Source

123 |

124 | You can download this project in either 125 | zip or 126 | tar formats. 127 |

128 |

You can also clone the project with Git by running: 129 |

$ git clone git://github.com/gpc/grails-audit-logging-plugin
130 |

131 | 132 |
133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /plugin/src/docs/upgrading.adoc: -------------------------------------------------------------------------------- 1 | == Upgrading to 5.0.x 2 | 3 | ** The 5.0.0 version has some significant changes to the 4.0.x version. 4 | * The groupId of the plugin has changed from org.grails.plugin to com.symentis 5 | * The configuration keys are the same as in 4.0.x 6 | * The mechanism to audit log transaction data has changed. We now participate in transactions and queue up audit logs. The audit logs are flushed with the current transaction. If the current transaction fails, no audit logs of this transaction are committed. Due to this major change, the plugin is currently not ORM agnostic and for now depends on Hibernate. As the plugin itself does not support other GORM implementations like Mongo (see #179), this shouldn't affect current projects. 7 | 8 | == Upgrading to 4.0.x 9 | 10 | The 4.0.0 version of the plugin is backwards compatible with 3.0.x. Any existing configuration and customization should work out of the box. 11 | 12 | == Upgrading to 3.0.x 13 | 14 | The 3.0.x version of the plugin is a major rewrite of the codebase. Therefore you need to upgrade your code for this version. 15 | 16 | === Domain Classes 17 | Prior versions of the plugin used a static property syntax to enable and customize logging: 18 | 19 | ```groovy 20 | // Legacy enable/disable 21 | static auditable = true 22 | 23 | // Legacy whitelist 24 | static auditable = [auditableProperties: ['name', 'famous', 'lastUpdated']] 25 | ``` 26 | 27 | Starting with version 3.0.0, the plugin uses an `Auditable` trait and/or a `Stampable` trait as both a marker as well as a way to configure and override auditing behavior. 28 | 29 | For the first example, just adding `implements Auditable` will enable the default behavior. 30 | 31 | ```groovy 32 | import grails.plugins.orm.auditable.Auditable 33 | class MyDomain implements Auditable { 34 | .. 35 | } 36 | ``` 37 | 38 | For the second example above, you simply override the appropriate method on the `Auditable` trait, in this case the method is `getLogIncluded()`: 39 | 40 | ```groovy 41 | @Override 42 | Collection getLogIncluded() { 43 | ['name', 'famous', 'lastUpdated'] 44 | } 45 | ``` 46 | 47 | NOTE: The getter methods on `Auditable` all follow the `getLog*()` format to minimize collisions with user methods. Typically they are `getLog()` followed by whatever the configurable value (included, excluded, mask) with a few exceptions. 48 | 49 | === Configuration Changes 50 | To support unifying the configuration, trait, and context usage, some of the configuration properties have been renamed. The following table should help to map old values and types to the new ones: 51 | 52 | [width="100%",options="header,footer"] 53 | |==================== 54 | | Prior Name | New Name | New Type 55 | | auditableProperties 56 | | included 57 | | Collection 58 | 59 | | defaultIgnore 60 | | excluded 61 | | Collection 62 | 63 | | ignore 64 | | excluded 65 | | Collection 66 | 67 | | defaultMask 68 | | mask 69 | | Collection 70 | 71 | | nonVerboseDelete 72 | | verboseEvents 73 | | Collection 74 | 75 | | transactional 76 | | *Removed* 77 | | Inherits existing transactionality 78 | 79 | | actorClosure 80 | | *Removed* 81 | | Use AuditRequestResolver 82 | 83 | | actorKey 84 | | *Removed* 85 | | Use AuditRequestResolver 86 | 87 | | sessionAttribute 88 | | *Removed* 89 | | Use AuditRequestResolver 90 | 91 | | handlersOnly 92 | | *Removed* 93 | | Use Grails callback methods 94 | 95 | | stampAlways 96 | | *Removed* 97 | | Use a `TraitInjector` with `Stampable` 98 | |==================== 99 | 100 | === Transactional Behavior 101 | Previously, the audit logging plugin had a `transactional` flag that indicated whether to include the audit log saves in a transaction. However, the audit logging plugin should really just participate (or not) in existing transactions and not make any attempts to control transactionality at that grain. For most usage, you want your Audit domain instances to be atomically created in the same transaction as the changes that are triggering them. If there's no existing transaction for the changes, it's not clear why there __should__ be a transaction just for the audit events or vice versa. 102 | 103 | The Audit instances are still saved in a new session within any existing transaction. 104 | 105 | === Handlers Removed 106 | The handler callbacks such as `onChange`, `onSave`, etc. have been removed starting with version 3.0.0. 107 | 108 | This behavior is already provided by Grails now using the `before*` and `after*` callback methods. 109 | 110 | For example, you could do something like: 111 | 112 | ```groovy 113 | def beforeInsert() { 114 | def dirtyAudit = getAuditableDirtyPropertyNames() 115 | 116 | // Do something special if certain properties are dirty, etc. 117 | } 118 | ``` 119 | 120 | === Actor Closure 121 | The actor closure has been replaced with a more formalized `AuditRequestResolver` strategy for resolving actor and URI information. 122 | 123 | By default, the plugin uses the `DefaultAuditRequestResolver`, which gets Principal information from the current Grails web request context. This is essentially the same as the prior default actor closure. 124 | 125 | If your application uses Spring Security, the plugin will register an instance of the `SpringSecurityRequestResolver` which will use the `springSecurityService` to resolve the current principal. 126 | 127 | For other security frameworks, you can implement the `AuditRequestResolver` interface and register a bean named `auditRequestResolver` to override the resolver provided by the plugin. 128 | 129 | === Stampable Trait 130 | The `@Stamp` annotation was removed in favor of a `Stampable` trait. This keeps the usage more in line with the direction of Grails in general as well as simplifying the implementation and usage. 131 | 132 | However, there are some limitations to implementing this as a trait: 133 | 134 | * Property names are `dateCreated, lastUpdated, createdBy, lastUpdatedBy` and are not configurable 135 | * If you want to customized the constraints, you must do so in your domain class 136 | * The values are technically populated on the `ValidationEvent` since they are non-nullable and must be populated prior to the default property validation. 137 | 138 | The `stampAlways` has been removed. If you want to mark *all* of your domain objects as stampable, you could define the following `TraitInjector`: 139 | 140 | ```groovy 141 | @CompileStatic 142 | class StampableTraitInjector implements TraitInjector { 143 | @Override 144 | Class getTrait() { 145 | Stampable 146 | } 147 | 148 | @Override 149 | String[] getArtefactTypes() { 150 | ['Domain'] as String[] 151 | } 152 | } 153 | ``` 154 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/AuditEventType.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.orm.auditable 2 | 3 | import groovy.transform.CompileStatic 4 | import org.grails.datastore.mapping.engine.event.EventType 5 | 6 | /** 7 | * Simple enum for audit events 8 | */ 9 | @CompileStatic 10 | enum AuditEventType { 11 | INSERT, UPDATE, DELETE 12 | 13 | @Override 14 | String toString() { 15 | name() 16 | } 17 | 18 | static AuditEventType forEventType(EventType type) { 19 | switch(type) { 20 | case EventType.PostInsert: 21 | return INSERT 22 | case EventType.PreDelete: 23 | return DELETE 24 | case EventType.PreUpdate: 25 | return UPDATE 26 | default: 27 | throw new IllegalArgumentException("Unexpected event type $type") 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLogContext.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package grails.plugins.orm.auditable 20 | 21 | import org.springframework.core.NamedThreadLocal 22 | 23 | /** 24 | * Allows runtime overriding of any audit logging configurable property 25 | */ 26 | class AuditLogContext { 27 | static final ThreadLocal auditLogConfig = new NamedThreadLocal("auditLog.context") 28 | 29 | /** 30 | * Overrides most 'grails.plugin.auditLog' configuration properties for execution of the given block 31 | * 32 | * For example: 33 | * 34 | * withConfig(logIncludes: ['foo', 'bar']) { ... } 35 | * withConfig(logFullClassName: false) { ... } 36 | * withConfig(verbose: false) { ... } 37 | * 38 | */ 39 | static withConfig(Map config, Closure block) { 40 | def previousValue = null 41 | try { 42 | // First getting and then setting should be okay as we use a ThreadLocal so no race conditions should be possible 43 | previousValue = auditLogConfig.get() 44 | auditLogConfig.set(mergeConfig(config)) 45 | block.call() 46 | } 47 | finally { 48 | auditLogConfig.set(previousValue) 49 | } 50 | } 51 | 52 | /** 53 | * Disable verbose audit logging for anything within this block 54 | */ 55 | static withoutVerboseAuditLog(Closure block) { 56 | withConfig([verbose: false], block) 57 | } 58 | 59 | /** 60 | * Disable audit logging for this block 61 | */ 62 | static withoutAuditLog(Closure block) { 63 | withConfig([disabled: true], block) 64 | } 65 | 66 | /** 67 | * @return the current context 68 | */ 69 | static Map getContext() { 70 | mergeConfig(auditLogConfig.get()) 71 | } 72 | 73 | private static Map mergeConfig(Map localConfig) { 74 | AuditLoggingConfigUtils.auditConfig + (localConfig ?: [:]) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLogListenerUtil.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package grails.plugins.orm.auditable 20 | 21 | import grails.gorm.validation.ConstrainedProperty 22 | import grails.gorm.validation.PersistentEntityValidator 23 | import grails.util.Holders 24 | import groovy.transform.CompileStatic 25 | import groovy.util.logging.Slf4j 26 | import org.grails.datastore.gorm.GormEntity 27 | import org.grails.datastore.mapping.model.MappingContext 28 | import org.grails.datastore.mapping.model.PersistentEntity 29 | import org.grails.datastore.mapping.model.PersistentProperty 30 | import org.grails.datastore.mapping.model.types.ToMany 31 | 32 | /** 33 | * Provides AuditLogListener utilities that 34 | * can be used as either templates for your own extensions to 35 | * the plugin or as default utilities. 36 | */ 37 | @Slf4j 38 | @CompileStatic 39 | class AuditLogListenerUtil { 40 | 41 | /** 42 | * Get the original or persistent or original value for the given domain.property. This method includes 43 | * some special case handling for hasMany properties, which don't follow normal rules. 44 | * 45 | * By default, getPersistentValue() is used to obtain the value. 46 | * If the value is always NULL, you can set AuditLogConfig.usePersistentDirtyPropertyValue = false 47 | * In this case, DirtyCheckable.html#getOriginalValue() is used. 48 | * 49 | * @see GormEntity#getPersistentValue(java.lang.String) 50 | * @see org.grails.datastore.mapping.dirty.checking.DirtyCheckable#getOriginalValue(java.lang.String) 51 | */ 52 | static Object getOriginalValue(Auditable domain, String propertyName) { 53 | PersistentEntity entity = getPersistentEntity(domain) 54 | PersistentProperty property = entity.getPropertyByName(propertyName) 55 | ConfigObject config = AuditLoggingConfigUtils.getAuditConfig() 56 | boolean usePersistentDirtyPropertyValues = config.getProperty("usePersistentDirtyPropertyValues") 57 | if (usePersistentDirtyPropertyValues) { 58 | property instanceof ToMany ? "N/A" : ((GormEntity)domain).getPersistentValue(propertyName) 59 | } 60 | else { 61 | property instanceof ToMany ? "N/A" : ((GormEntity)domain).getOriginalValue(propertyName) 62 | } 63 | } 64 | 65 | /** 66 | * Helper method to make a map of the current property values 67 | * 68 | * @param propertyNames 69 | * @param domain 70 | * @return 71 | */ 72 | static Map makeMap(Collection propertyNames, Auditable domain) { 73 | propertyNames.collectEntries { [it, domain.metaClass.getProperty(domain, it)] } 74 | } 75 | 76 | /** 77 | * Return the grails domain class for the given domain object. 78 | * 79 | * @param domain the domain instance 80 | */ 81 | static PersistentEntity getPersistentEntity(domain) { 82 | Holders.grailsApplication.mappingContext.getPersistentEntity(domain.getClass().name) 83 | } 84 | 85 | /** 86 | * @param domain the auditable domain object 87 | * @param propertyName property name 88 | * @param value the value of the property 89 | * @return 90 | */ 91 | static String conditionallyMaskAndTruncate(Auditable domain, String propertyName, String value, Integer maxLength) { 92 | if (value == null) { 93 | return null 94 | } 95 | 96 | // Always trim any space 97 | value = value.trim() 98 | 99 | if (domain.logMaskProperties && domain.logMaskProperties.contains(propertyName)) { 100 | return AuditLogContext.context.propertyMask as String ?: '********' 101 | } 102 | if (maxLength && value.length() > maxLength) { 103 | return value.substring(0, maxLength) 104 | } 105 | 106 | value 107 | } 108 | 109 | /** 110 | * Determine the truncateLength based on the configured truncateLength and the actual auditDomainClass maxSize constraint for newValue. 111 | */ 112 | static Integer determineTruncateLength() { 113 | String confAuditDomainClassName = AuditLoggingConfigUtils.auditConfig.getProperty('auditDomainClassName') 114 | if (!confAuditDomainClassName) { 115 | throw new IllegalArgumentException("Please configure auditLog.auditDomainClassName in Config.groovy") 116 | } 117 | 118 | MappingContext mappingContext = Holders.grailsApplication.mappingContext 119 | PersistentEntity auditPersistentEntity = mappingContext.getPersistentEntity(confAuditDomainClassName) 120 | if (!auditPersistentEntity) { 121 | throw new IllegalArgumentException("The configured audit logging domain class '$confAuditDomainClassName' is not a domain class") 122 | } 123 | 124 | // Get the property constraints 125 | PersistentEntityValidator entityValidator = mappingContext.getEntityValidator(auditPersistentEntity) as PersistentEntityValidator 126 | Map constrainedProperties = (entityValidator?.constrainedProperties ?: [:]) as Map 127 | 128 | // The configured length is the min size of oldValue, newValue, or configured truncateLength 129 | Integer oldValueMaxSize = constrainedProperties['oldValue'].maxSize ?: 255 130 | Integer newValueMaxSize = constrainedProperties['newValue'].maxSize ?: 255 131 | Integer maxSize = Math.min(oldValueMaxSize, newValueMaxSize) 132 | Integer configuredTruncateLength = (AuditLoggingConfigUtils.auditConfig.getProperty('truncateLength') ?: Integer.MAX_VALUE) as Integer 133 | 134 | Math.min(maxSize, configuredTruncateLength) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLogQueueManager.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.orm.auditable 2 | 3 | import groovy.transform.CompileStatic 4 | import groovy.util.logging.Slf4j 5 | import org.grails.datastore.gorm.GormEntity 6 | import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent 7 | import org.grails.orm.hibernate.HibernateDatastore 8 | import org.hibernate.Transaction 9 | import org.hibernate.action.spi.AfterTransactionCompletionProcess 10 | import org.hibernate.engine.spi.SharedSessionContractImplementor 11 | import org.hibernate.internal.SessionImpl 12 | 13 | import java.util.concurrent.ConcurrentHashMap 14 | 15 | /** 16 | * Queue audit logging changes for flushing on transaction commit to ensure proper transactional semantics 17 | * 18 | * @author Aaron Long 19 | */ 20 | @Slf4j 21 | @CompileStatic 22 | class AuditLogQueueManager { 23 | 24 | private static final Map auditProcesses = new ConcurrentHashMap<>() 25 | 26 | static void addToQueue(GormEntity auditInstance, AbstractPersistenceEvent event) { 27 | if (!(event.source instanceof HibernateDatastore)) { 28 | log.warn("Can't handle audit event for entity from unsupported datastore ${event.source.class.name}") 29 | return 30 | } 31 | SessionImpl session = (SessionImpl) ((HibernateDatastore) event.source).sessionFactory.currentSession 32 | 33 | if (!session.transactionInProgress) { 34 | // Seems like no transaction is active 35 | // => Save audit entry right now 36 | // In Hibernate > 5.2 this can only happen by setting `hibernate.allow_update_outside_transaction: true` 37 | // 38 | // When audit domain class is in same datastore as a newly INSERTed entity we can't cause a flush of the session here. 39 | // This would cause a Hibernate AssertionFailure: "null id in test.Author entry (don't flush the Session after an exception occurs)" 40 | // 41 | // => we can't cause a session flush of the session that is used to flush the changes to the observed entity 42 | // => we can't use withNewTransaction because it reuses the current session 43 | // => we should still use a transaction because in theory the audit domain could be in another datastore 44 | // where allow_update_outside_transaction isn't set 45 | // 46 | // => use withNewSession + withNewTransaction 47 | auditInstance.invokeMethod("withNewSession") { 48 | auditInstance.invokeMethod("withTransaction") { 49 | auditInstance.save(failOnError: true) 50 | } 51 | } 52 | return 53 | } 54 | 55 | 56 | Transaction transaction = (Transaction) session.transaction 57 | 58 | AuditLogTransactionSynchronization auditProcess = auditProcesses[transaction] 59 | if (auditProcess == null) { 60 | auditProcess = auditProcesses[transaction] = new AuditLogTransactionSynchronization() 61 | // Roughly this is like a Spring transaction synchronization (TransactionSynchronizationManager.registerSynchronization) 62 | // The difference is that even when using nested transactions like 63 | // DomainInDatastoreOne.withNewTransaction { 64 | // DomainInDatastoreTwo.withNewTransaction { 65 | // + flush 66 | // } 67 | // } 68 | // we are able to register a synchroniation on the OUTER transaction even though this code is running while the 69 | // INNER transaction is active. 70 | // 71 | // Just using Spring Transactions and/or GORM would be better because it would allow to be datastore agnostic 72 | // But simply using TransactionSynchronizationManager.registerSynchronization doesn't work because it would 73 | // register the synchronisation in the innermost transaction. 74 | // 75 | // TODO: Find GORM agnostic way of doing this 76 | // If we don't find a GORM agnostic way we need to abstract this implementation away e.g. auditlogging-hibernate 77 | session.actionQueue.registerProcess( 78 | new AfterTransactionCompletionProcess() { 79 | @Override 80 | void doAfterTransactionCompletion(boolean success, SharedSessionContractImplementor session2) { 81 | if (success && auditProcesses.remove(transaction)) { 82 | auditProcess.afterCommit() 83 | } 84 | } 85 | } 86 | ) 87 | } 88 | 89 | auditProcess.addToQueue(auditInstance) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLogTransactionSynchronization.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.orm.auditable 2 | 3 | import groovy.transform.CompileStatic 4 | import groovy.util.logging.Slf4j 5 | import org.grails.datastore.gorm.GormEntity 6 | 7 | @Slf4j 8 | @CompileStatic 9 | class AuditLogTransactionSynchronization { 10 | private List pendingAuditInstances = [] 11 | 12 | void addToQueue(GormEntity auditInstance) { 13 | // Otherwise, if we have a transaction queue this instance. 14 | // Save them all when the transaction where changes where made commits. 15 | pendingAuditInstances << auditInstance 16 | log.trace("Added {} to synchronization queue", auditInstance) 17 | } 18 | 19 | void afterCommit() { 20 | if (!pendingAuditInstances) { 21 | return 22 | } 23 | try { 24 | log.debug("Writing {} pending audit instances in afterCommit()", pendingAuditInstances.size()) 25 | // Use withNewSession + withTransaction here as well to be completely independent from user session 26 | pendingAuditInstances[0].invokeMethod("withNewSession") { 27 | pendingAuditInstances[0].invokeMethod("withTransaction") { 28 | for (GormEntity entity in pendingAuditInstances) { 29 | entity.save(failOnError: true) 30 | } 31 | } 32 | } 33 | } 34 | finally { 35 | pendingAuditInstances.clear() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLoggingConfigUtils.groovy: -------------------------------------------------------------------------------- 1 | /* Copyright 2006-2015 the original author or authors. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package grails.plugins.orm.auditable 16 | 17 | import grails.core.GrailsApplication 18 | import grails.util.Environment 19 | import groovy.util.logging.Slf4j 20 | 21 | /** 22 | * Plugin config Helper methods. 23 | * 24 | * @author Robert Oschwald 25 | */ 26 | @Slf4j 27 | class AuditLoggingConfigUtils { 28 | private static ConfigObject _auditConfig 29 | private static GrailsApplication application 30 | 31 | // Constructor. Static methods only 32 | private AuditLoggingConfigUtils() {} 33 | 34 | /** 35 | * Parse and load the auditLog configuration. 36 | * @return the configuration 37 | */ 38 | static synchronized ConfigObject getAuditConfig() { 39 | if (_auditConfig == null) { 40 | log.trace 'Building auditLog config since there is no cached config' 41 | reloadAuditConfig() 42 | } 43 | _auditConfig 44 | } 45 | 46 | /** 47 | * For testing only. 48 | * @param config the config 49 | */ 50 | static void setAuditConfig(ConfigObject config) { 51 | _auditConfig = config 52 | } 53 | 54 | /** Reset the config for testing or after a dev mode Config.groovy change. */ 55 | static synchronized void resetAuditConfig() { 56 | _auditConfig = null 57 | log.trace 'reset auditLog config' 58 | } 59 | 60 | /** 61 | * Allow a secondary plugin to add config attributes. 62 | * @param className the name of the config class. 63 | */ 64 | static synchronized void loadSecondaryConfig(String className) { 65 | mergeConfig auditConfig, className 66 | log.trace 'loaded secondary config {}', className 67 | } 68 | 69 | /** Force a reload of the auditLog configuration. */ 70 | static void reloadAuditConfig() { 71 | mergeConfig ReflectionUtils.auditConfig, 'DefaultAuditLogConfig' 72 | log.trace 'reloaded auditLog config' 73 | } 74 | 75 | /** 76 | * Merge in a secondary config (provided by plugin as defaults) into the main config. 77 | * @param currentConfig the current configuration 78 | * @param className the name of the config class to load 79 | */ 80 | private static void mergeConfig(ConfigObject currentConfig, String className) { 81 | log.trace("Merging currentConfig with $className") 82 | GroovyClassLoader classLoader = new GroovyClassLoader(Thread.currentThread().contextClassLoader) 83 | ConfigObject secondary = new ConfigSlurper(Environment.current.name).parse(classLoader.loadClass(className)) 84 | secondary = secondary.defaultAuditLog as ConfigObject 85 | 86 | Collection keysToDefaultEmpty = [] 87 | findKeysToDefaultEmpty secondary, '', keysToDefaultEmpty 88 | 89 | ConfigObject merged = mergeConfig(currentConfig, secondary) 90 | 91 | // having discovered the keys that have map values (since they initially point to empty maps), 92 | // check them again and remove the damage done when Map values are 'flattened' 93 | for (String key in keysToDefaultEmpty) { 94 | Map value = (Map) ReflectionUtils.getConfigProperty(key, merged) 95 | for (Iterator iter = value.entrySet().iterator(); iter.hasNext();) { 96 | def entry = iter.next() 97 | if (entry.value instanceof Map) { 98 | iter.remove() 99 | } 100 | } 101 | } 102 | 103 | _auditConfig = ReflectionUtils.auditConfig = merged 104 | } 105 | 106 | /** 107 | * Merge two configs together. The order is important if secondary is not null then 108 | * start with that and merge the main config on top of that. This lets the secondary 109 | * config act as default values but let user-supplied values in the main config override them. 110 | * 111 | * @param currentConfig the main config, starting from Config.groovy 112 | * @param secondary new default values 113 | * @return the merged configs 114 | */ 115 | private static ConfigObject mergeConfig(ConfigObject currentConfig, ConfigObject secondary) { 116 | log.trace("Merging secondary config on top of currentConfig") 117 | (secondary ?: new ConfigObject()).merge(currentConfig ?: new ConfigObject()) as ConfigObject 118 | } 119 | 120 | /** 121 | * Given an unmodified config map with defaults, loop through the keys looking for values that are initially 122 | * empty maps. This will be used after merging to remove map values that cause problems by being included both as 123 | * the result from the ConfigSlurper (which is correct) and as a "flattened" maps which confuse Audit Logging. 124 | * @param m the config map 125 | * @param fullPath the path to this config map, e.g. 'grails.plugin.auditLog 126 | * @param keysToDefaultEmpty a collection of key names to add to 127 | */ 128 | private static void findKeysToDefaultEmpty(Map m, String fullPath, Collection keysToDefaultEmpty) { 129 | m.each { k, v -> 130 | if (v instanceof Map) { 131 | if (v) { 132 | // recurse 133 | findKeysToDefaultEmpty((Map) v, fullPath + '.' + k, keysToDefaultEmpty) 134 | } 135 | else { 136 | // since it's an empty map, capture its path for the cleanup pass 137 | keysToDefaultEmpty << (fullPath + '.' + k).substring(1) 138 | } 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLoggingGrailsPlugin.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package grails.plugins.orm.auditable 20 | 21 | import grails.plugins.Plugin 22 | import grails.plugins.orm.auditable.resolvers.DefaultAuditRequestResolver 23 | import grails.plugins.orm.auditable.resolvers.SpringSecurityRequestResolver 24 | import groovy.util.logging.Slf4j 25 | import org.grails.datastore.mapping.core.Datastore 26 | import org.springframework.beans.factory.NoSuchBeanDefinitionException 27 | /** 28 | * @author Robert Oschwald 29 | * @author Aaron Long 30 | * @author Shawn Hartsock 31 | * @author Graeme Rocher 32 | * 33 | * Credit is due to the following other projects, 34 | * first is Kevin Burke's HibernateEventsGrailsPlugin 35 | * second is the AuditLogging post by Rob Monie at 36 | * http://www.hibernate.org/318.html 37 | * 38 | * See Documentation: 39 | * https://github.com/gpc/grails-audit-logging-plugin 40 | * 41 | */ 42 | @Slf4j 43 | @SuppressWarnings("GroovyUnusedDeclaration") 44 | class AuditLoggingGrailsPlugin extends Plugin { 45 | def grailsVersion = '7.0.0 > *' 46 | 47 | def title = "Audit Logging Plugin" 48 | def authorEmail = "roos@symentis.com" 49 | def description = """ 50 | Automatically log change events for domain objects. Optionally supports Stamping. This plugin is ORM agnostic. 51 | """ 52 | String documentation = 'https://github.com/gpc/grails-audit-logging-plugin' 53 | String license = 'APACHE' 54 | 55 | def developers = [ 56 | [name: 'Robert Oschwald', email: 'roos@symentis.com'], 57 | [name: 'Elmar Kretzer', email: 'elkr@symentis.com'], 58 | [name: 'Aaron Long', email: 'longwa@gmail.com'] 59 | ] 60 | 61 | def issueManagement = [system: 'GitHub', url: 'https://github.com/gpc/grails-audit-logging-plugin/issues'] 62 | def scm = [url: 'https://github.com/gpc/grails-audit-logging-plugin'] 63 | def loadAfter = ['core', 'dataSource', 'springSecurityCore'] 64 | 65 | // Register generic GORM listener 66 | @Override 67 | void doWithApplicationContext() { 68 | ConfigObject config = AuditLoggingConfigUtils.auditConfig 69 | 70 | // Allow a collection of ignored data store names 71 | Set excludedDataStores = (config.getProperty("excludedDataSources") ?: []) as Set 72 | 73 | applicationContext.getBeansOfType(Datastore).each { String key, Datastore datastore -> 74 | 75 | //Retrieve all dataSources being managed by the DataStore within the application (i.e. child DataStores) 76 | Map dataSources = datastore.metaClass.getProperty(datastore, 'datastoresByConnectionSource') 77 | 78 | //Iterate through each DataSource to determine if they should have an AuditLogListener or StampListener instantiated for them 79 | dataSources.each { 80 | //Retrieve the specific childDataStore for a DataSource 81 | Datastore childDataStore = datastore.getDatastoreForConnection(it.getKey().toString()) 82 | 83 | // mongo datastores don't have a dataSourceName property 84 | if (childDataStore.getProperties().containsKey("dataSourceName")) { 85 | 86 | String dataSourceName = childDataStore.metaClass.getProperty(datastore, 'dataSourceName') 87 | 88 | if (!config.disabled && !excludedDataStores.contains(dataSourceName)) { 89 | applicationContext.addApplicationListener(new AuditLogListener(childDataStore, grailsApplication)) 90 | } 91 | if (config.stampEnabled && !excludedDataStores.contains(dataSourceName)) { 92 | applicationContext.addApplicationListener(new StampListener(childDataStore, grailsApplication)) 93 | } 94 | } else { 95 | if (!config.disabled) { 96 | applicationContext.addApplicationListener(new AuditLogListener(childDataStore, grailsApplication)) 97 | } 98 | if (config.stampEnabled) { 99 | applicationContext.addApplicationListener(new StampListener(childDataStore, grailsApplication)) 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | @Override 107 | Closure doWithSpring() {{-> 108 | // Must load config before the application context has been refreshed 109 | AuditLoggingConfigUtils.resetAuditConfig() 110 | AuditLoggingConfigUtils.reloadAuditConfig() 111 | 112 | try { 113 | if (applicationContext.getBean("springSecurityService")) { 114 | log.debug("Audit logging detected spring security, using spring security request resolver") 115 | auditRequestResolver(SpringSecurityRequestResolver) { 116 | springSecurityService = ref('springSecurityService') 117 | } 118 | } 119 | } 120 | catch(NoSuchBeanDefinitionException ignored) { 121 | log.debug("Audit logging using default request resolver") 122 | auditRequestResolver(DefaultAuditRequestResolver) 123 | } 124 | }} 125 | 126 | @Override 127 | void onConfigChange(Map event) { 128 | AuditLoggingConfigUtils.resetAuditConfig() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/ReflectionUtils.groovy: -------------------------------------------------------------------------------- 1 | /* Copyright 2006-2015 the original author or authors. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package grails.plugins.orm.auditable 16 | 17 | import grails.config.Config 18 | import grails.core.GrailsApplication 19 | import grails.util.Holders 20 | import groovy.util.logging.Slf4j 21 | import org.grails.config.NavigableMap 22 | import org.grails.config.PropertySourcesConfig 23 | import org.springframework.core.env.MapPropertySource 24 | import org.springframework.core.env.PropertySource 25 | /** 26 | * Helper methods that use dynamic Groovy 27 | */ 28 | @Slf4j 29 | class ReflectionUtils { 30 | 31 | // set at startup 32 | static GrailsApplication application 33 | 34 | private ReflectionUtils() { 35 | // static only 36 | } 37 | 38 | static Object getConfigProperty(String name, config = AuditLoggingConfigUtils.auditConfig) { 39 | def value = config 40 | name.split('\\.').each { String part -> value = value."$part" } 41 | value 42 | } 43 | 44 | static void setConfigProperty(String name, value) { 45 | def config = AuditLoggingConfigUtils.auditConfig 46 | 47 | List parts = name.split('\\.') 48 | name = parts.remove(parts.size() - 1) 49 | 50 | parts.each { String part -> config = config."$part" } 51 | 52 | config."$name" = value 53 | } 54 | 55 | static List asList(Object o) { 56 | o ? o as List : [] 57 | } 58 | 59 | static ConfigObject getAuditConfig() { 60 | Config grailsConfig = getApplication().config 61 | if (grailsConfig.getProperty('auditLog', NavigableMap)) { 62 | log.error "Your auditLog configuration settings use the old prefix 'auditLog' but must now use 'grails.plugin.auditLog'" 63 | } 64 | grailsConfig.getProperty("grails.plugin.auditLog", NavigableMap) as ConfigObject 65 | } 66 | 67 | static void setAuditConfig(ConfigObject c) { 68 | ConfigObject config = new ConfigObject() 69 | config.grails.plugin.auditLog = c 70 | 71 | PropertySource propertySource = new MapPropertySource('AuditConfig', [:] << config) 72 | def propertySources = application.mainContext.environment.propertySources 73 | propertySources.addFirst propertySource 74 | 75 | getApplication().config = new PropertySourcesConfig(propertySources) 76 | } 77 | 78 | private static GrailsApplication getApplication() { 79 | if (!application) { 80 | application = Holders.grailsApplication 81 | } 82 | application 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/StampListener.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package grails.plugins.orm.auditable 20 | 21 | import grails.core.GrailsApplication 22 | import grails.plugins.orm.auditable.resolvers.AuditRequestResolver 23 | import groovy.transform.CompileStatic 24 | import groovy.util.logging.Slf4j 25 | import org.grails.datastore.mapping.core.Datastore 26 | import org.grails.datastore.mapping.engine.EntityAccess 27 | import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent 28 | import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener 29 | import org.grails.datastore.mapping.engine.event.EventType 30 | import org.grails.datastore.mapping.engine.event.PreInsertEvent 31 | import org.grails.datastore.mapping.engine.event.PreUpdateEvent 32 | import org.springframework.context.ApplicationEvent 33 | 34 | /** 35 | * Grails interceptor for logging saves, updates, deletes and acting on 36 | * individual properties changes and delegating calls back to the Domain Class 37 | */ 38 | @Slf4j 39 | @CompileStatic 40 | class StampListener extends AbstractPersistenceEventListener { 41 | private GrailsApplication grailsApplication 42 | 43 | StampListener(Datastore datastore, GrailsApplication grailsApplication) { 44 | super(datastore) 45 | this.grailsApplication = grailsApplication 46 | } 47 | 48 | @Override 49 | protected void onPersistenceEvent(AbstractPersistenceEvent event) { 50 | if (event.source != datastore) { 51 | log.trace("Event received for datastore {}, ignoring", event.source) 52 | return 53 | } 54 | if (!(event.entityObject instanceof Stampable)) { 55 | return 56 | } 57 | 58 | // See if we are disabled for this context 59 | if (!AuditLogContext.context.stampEnabled) { 60 | return 61 | } 62 | 63 | try { 64 | log.trace("Stamping object {}", event.entityObject.class.name) 65 | 66 | // Lookup the request resolver here to ensure that applications have a chance 67 | // to override this bean to provide different strategies 68 | AuditRequestResolver requestResolver = grailsApplication.mainContext.getBean(AuditRequestResolver) 69 | 70 | switch(event.eventType) { 71 | case EventType.PreInsert: 72 | handleInsert(event.entityAccess, requestResolver) 73 | break 74 | case EventType.PreUpdate: 75 | handleUpdate(event.entityAccess, requestResolver) 76 | break 77 | } 78 | } 79 | catch (Exception e) { 80 | if (AuditLogContext.context.failOnError) { 81 | throw e 82 | } 83 | else { 84 | log.error("Error stamping domain ${event.entityObject}", e) 85 | } 86 | } 87 | } 88 | 89 | @Override 90 | boolean supportsEventType(Class eventType) { 91 | eventType.isAssignableFrom(PreInsertEvent) || eventType.isAssignableFrom(PreUpdateEvent) 92 | } 93 | 94 | /** 95 | * Stamp inserts 96 | */ 97 | protected void handleInsert(EntityAccess domain, AuditRequestResolver requestResolver) { 98 | // Set actors, Grails will take care of setting the dates 99 | String currentActor = requestResolver.currentActor 100 | domain.setProperty("createdBy", currentActor) 101 | domain.setProperty("lastUpdatedBy", currentActor) 102 | } 103 | 104 | /** 105 | * Stamp updates 106 | */ 107 | protected void handleUpdate(EntityAccess domain, AuditRequestResolver requestResolver) { 108 | domain.setProperty("lastUpdatedBy", requestResolver.currentActor) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/Stampable.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.orm.auditable 2 | 3 | import org.grails.datastore.gorm.GormEntity 4 | 5 | /** 6 | * Entities should implement this trait to provide automatic stamping of date and user information 7 | */ 8 | trait Stampable extends GormEntity { 9 | // Grails will automatically populate these 10 | Date dateCreated 11 | Date lastUpdated 12 | 13 | // We initialize these to non-null to they pass initial validation, they are set on insert/update 14 | String createdBy = "N/A" 15 | String lastUpdatedBy = "N/A" 16 | } 17 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/resolvers/AuditRequestResolver.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.orm.auditable.resolvers 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @CompileStatic 6 | interface AuditRequestResolver { 7 | /** 8 | * @return the current actor 9 | */ 10 | String getCurrentActor() 11 | 12 | /** 13 | * @return the current request URI or null if no active request 14 | */ 15 | String getCurrentURI() 16 | } 17 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/resolvers/DefaultAuditRequestResolver.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.orm.auditable.resolvers 2 | 3 | import grails.plugins.orm.auditable.AuditLogContext 4 | import groovy.transform.CompileStatic 5 | import org.grails.web.servlet.mvc.GrailsWebRequest 6 | 7 | @CompileStatic 8 | class DefaultAuditRequestResolver implements AuditRequestResolver { 9 | @Override 10 | String getCurrentActor() { 11 | GrailsWebRequest request = GrailsWebRequest.lookup() 12 | request?.remoteUser ?: request?.userPrincipal?.name ?: AuditLogContext.context.defaultActor ?: 'N/A' 13 | } 14 | 15 | @Override 16 | String getCurrentURI() { 17 | GrailsWebRequest request = GrailsWebRequest.lookup() 18 | request?.request?.requestURI 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/orm/auditable/resolvers/SpringSecurityRequestResolver.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.orm.auditable.resolvers 2 | 3 | /** 4 | * Default resolver that uses the SpringSecurityService principal if available 5 | */ 6 | class SpringSecurityRequestResolver extends DefaultAuditRequestResolver { 7 | def springSecurityService 8 | 9 | @Override 10 | String getCurrentActor() { 11 | springSecurityService?.currentUser?.toString() ?: super.getCurrentActor() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plugin/src/main/scripts/audit-quickstart.groovy: -------------------------------------------------------------------------------- 1 | /* Copyright 2006-2015 the original author or authors. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | import grails.codegen.model.Model 16 | import groovy.transform.Field 17 | 18 | @Field Map templateAttributes 19 | Model auditModel 20 | @Field String usageMessage = ''' 21 | grails audit-quickstart 22 | 23 | Example: grails audit-quickstart com.yourapp MyAuditLogEvent 24 | 25 | ''' 26 | 27 | description 'Creates domain classes and updates config settings for the Audit-Logging plugin', { 28 | 29 | usage usageMessage 30 | 31 | argument name:'Domain class package', description:'The package to use for the domain class', required:false 32 | } 33 | 34 | if (args.size() < 2) { 35 | error 'Usage:' + usageMessage 36 | return false 37 | } 38 | 39 | def argValues = parseArgs() 40 | if (!argValues) { 41 | return false 42 | } 43 | 44 | String packageName = args[0] 45 | auditModel = model(packageName + '.' + args[1]) 46 | 47 | String message = "Creating Audit domain class '" + auditModel.simpleName + "'" 48 | message += " in package '" + packageName + "'" 49 | addStatus message 50 | 51 | templateAttributes = [packageName :auditModel.packageName, 52 | auditClassName :auditModel.simpleName, 53 | auditClassProperty:auditModel.modelName] 54 | 55 | 56 | createDomain(auditModel) 57 | 58 | updateConfig(auditModel?.simpleName, auditModel?.packageName) 59 | 60 | addStatus ''' 61 | ******************************************************* 62 | * Created auditLogEvent domain class. * 63 | * Your grails-app/conf/application.groovy file has * 64 | * been updated with the class name of the configured * 65 | * domain class. * 66 | * Please verify that the values are correct. * 67 | ******************************************************* 68 | ''' 69 | 70 | 71 | private String[] parseArgs() { 72 | if (2 == args.size()) { 73 | return args 74 | } 75 | usage usageMessage 76 | null 77 | } 78 | 79 | private void createDomain(Model auditModel) { 80 | generateFile 'AuditLogEvent', auditModel.packagePath, auditModel.simpleName 81 | } 82 | 83 | private void updateConfig(String auditClassName, String packageName) { 84 | file("grails-app/conf/application.groovy").withWriterAppend { BufferedWriter writer -> 85 | writer.newLine() 86 | writer.newLine() 87 | writer.writeLine '// Added by the Audit-Logging plugin:' 88 | writer.writeLine "grails.plugin.auditLog.auditDomainClassName = '${packageName}.$auditClassName'" 89 | writer.newLine() 90 | } 91 | } 92 | 93 | private void generateFile(String templateName, String packagePath, String className) { 94 | render template(templateName + '.groovy.template'), 95 | file("grails-app/domain/$packagePath/${className}.groovy"), 96 | templateAttributes, false 97 | } 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /plugin/src/main/templates/AuditLogEvent.groovy.template: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package ${packageName} 20 | 21 | /** 22 | * ${auditClassName}s are reported to the AuditLog table. 23 | * This requires you to set up a table or allow 24 | * Grails to create a table for you. (e.g. DDL or db-migration plugin) 25 | */ 26 | class ${auditClassName} implements Serializable { 27 | static final long serialVersionUID = 1L 28 | 29 | String actor 30 | String uri 31 | String className 32 | String persistedObjectId 33 | Long persistedObjectVersion = 0 34 | 35 | String eventName 36 | String propertyName 37 | String oldValue 38 | String newValue 39 | 40 | Date dateCreated 41 | Date lastUpdated 42 | 43 | static constraints = { 44 | actor(nullable: true) 45 | uri(nullable: true) 46 | className(nullable: true) 47 | persistedObjectId(nullable: true) 48 | persistedObjectVersion(nullable: true) 49 | eventName(nullable: true) 50 | propertyName(nullable: true) 51 | oldValue(nullable: true) 52 | newValue(nullable: true) 53 | } 54 | 55 | static mapping = { 56 | 57 | // Set similiar when you used "auditLog.tablename" in < 1.1.0 plugin version. 58 | table 'audit_log' 59 | 60 | // Remove when you used "auditLog.cacheDisabled = true" in < 1.1.0 plugin version. 61 | cache usage: 'read-only', include: 'non-lazy' 62 | 63 | // Set similiar when you used "auditLog.useDatasource" in < 1.1.0 plugin version. 64 | // datasource "yourdatasource" 65 | 66 | // Set similiar when you used "auditLog.idMapping" in < 1.1.0 plugin version. Example: 67 | // id generator:"uuid2", type:"string", "length:36" 68 | 69 | // no HQL queries package name import (was default in 1.x version) 70 | // autoImport false 71 | 72 | version false 73 | autoTimestamp false 74 | 75 | // for large column support (as in < 1.0.6 plugin versions), use this 76 | // oldValue type: 'text' 77 | // newValue type: 'text' 78 | } 79 | 80 | /** 81 | * Deserializer that maps a stored map onto the object 82 | * assuming that the keys match attribute properties. 83 | */ 84 | private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { 85 | def map = input.readObject() 86 | map.each { k, v -> this."\$k" = v } 87 | } 88 | 89 | /** 90 | * Because Closures do not serialize we can't send the constraints closure 91 | * to the Serialize API so we have to have a custom serializer to allow for 92 | * this object to show up inside a webFlow context. 93 | */ 94 | private void writeObject(ObjectOutputStream out) throws IOException { 95 | def map = [ 96 | id: id, 97 | dateCreated: dateCreated, 98 | lastUpdated: lastUpdated, 99 | 100 | actor: actor, 101 | uri: uri, 102 | className: className, 103 | persistedObjectId: persistedObjectId, 104 | persistedObjectVersion: persistedObjectVersion, 105 | 106 | eventName: eventName, 107 | propertyName: propertyName, 108 | oldValue: oldValue, 109 | newValue: newValue, 110 | ] 111 | out.writeObject(map) 112 | } 113 | 114 | String toString() { 115 | String actorStr = actor ? "user \${actor}" : "user ?" 116 | "audit log \${dateCreated} \${actorStr} " + 117 | "\${eventName} \${className} " + 118 | "id:\${persistedObjectId} version:\${persistedObjectVersion}" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.gradle.develocity" version "$develocityVersion" 3 | id "com.gradle.common-custom-user-data-gradle-plugin" version "$customUserDataVersion" 4 | } 5 | 6 | def isCI = System.getenv().containsKey('CI') 7 | def isLocal = !isCI 8 | 9 | develocity { 10 | server = 'https://ge.grails.org' 11 | buildScan { 12 | tag('grails') 13 | tag('grails-forge') 14 | publishing.onlyIf { it.authenticated } 15 | uploadInBackground = isLocal 16 | } 17 | } 18 | 19 | buildCache { 20 | local { enabled = isLocal } 21 | remote(develocity.buildCache) { 22 | push = isCI 23 | enabled = true 24 | } 25 | } 26 | 27 | rootProject.name = 'grails-audit-logging' 28 | 29 | include 'plugin' 30 | include 'examples:audit-test' 31 | include 'examples:audit-test-allow-update-outside-transaction' 32 | 33 | findProject(":plugin").name = "audit-logging" 34 | --------------------------------------------------------------------------------