diff --git a/.envrc b/.envrc new file mode 100644 index 000000000000..3011114ccc9a --- /dev/null +++ b/.envrc @@ -0,0 +1,17 @@ +# direnv configuration for hcp-casdoor +# Sets up Go paths and installs dependencies on first enter +export GOPATH="${GOPATH:-$HOME/go}" +export GOBIN="${GOBIN:-$GOPATH/bin}" +export PATH="$GOBIN:$PATH" + +# Fetch Go modules (best-effort) +if command -v go >/dev/null 2>&1 && [ -f go.mod ]; then + go mod download || true +fi + +# Install web dependencies only if yarn is available and node_modules missing +if command -v yarn >/dev/null 2>&1 && [ ! -d web/node_modules ]; then + (cd web && yarn install --frozen-lockfile --network-timeout 1000000) || true +fi + +# You may run `direnv allow` to authorize this file (this is done automatically by the workflow below) diff --git a/.github/workflows/bootstrap-ecr.yml b/.github/workflows/bootstrap-ecr.yml new file mode 100644 index 000000000000..9239373f06f1 --- /dev/null +++ b/.github/workflows/bootstrap-ecr.yml @@ -0,0 +1,180 @@ +name: Bootstrap ECR + +on: + workflow_dispatch: + inputs: + integration_role_arn: + description: Integration AWS role ARN used by Doormat + type: string + required: false + default: arn:aws:iam::995429640676:role/DoormatGithubActionsRole + production_role_arn: + description: Production AWS role ARN used by Doormat + type: string + required: false + default: arn:aws:iam::877995958936:role/DoormatAdminRole + +permissions: + contents: read + id-token: write + +env: + AWS_REGION: "us-east-2" + ECR_STANDARD_REPOSITORY: "casdoor" + ECR_ALL_IN_ONE_REPOSITORY: "casdoor-all-in-one" + +jobs: + bootstrap-integration: + name: Bootstrap Integration ECR + runs-on: ubuntu-latest + env: + ACCOUNT_ID: "995429640676" + ROLE_ARN: ${{ inputs.integration_role_arn }} + TARGET_ENV: integration + outputs: + standard-repository-uri: ${{ steps.bootstrap.outputs.standard_repository_uri }} + standard-repository-arn: ${{ steps.bootstrap.outputs.standard_repository_arn }} + all-in-one-repository-uri: ${{ steps.bootstrap.outputs.all_in_one_repository_uri }} + all-in-one-repository-arn: ${{ steps.bootstrap.outputs.all_in_one_repository_arn }} + steps: + - name: Configure AWS Credentials + uses: hashicorp/doormat-action@690d0244e8d4751d3090f6785acac1b4b84d5223 + with: + aws-role-arn: ${{ env.ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Bootstrap Repositories + id: bootstrap + shell: bash + run: | + set -euo pipefail + + bootstrap_repository() { + local repository_name="$1" + + if ! aws ecr describe-repositories --region "$AWS_REGION" --repository-names "$repository_name" >/dev/null 2>&1; then + aws ecr create-repository \ + --region "$AWS_REGION" \ + --repository-name "$repository_name" \ + --image-scanning-configuration scanOnPush=true \ + >/dev/null + fi + + aws ecr describe-repositories \ + --region "$AWS_REGION" \ + --repository-names "$repository_name" \ + --query 'repositories[0].[repositoryUri,repositoryArn]' \ + --output text + } + + read -r standard_uri standard_arn < <(bootstrap_repository "$ECR_STANDARD_REPOSITORY") + read -r all_in_one_uri all_in_one_arn < <(bootstrap_repository "$ECR_ALL_IN_ONE_REPOSITORY") + + { + echo "standard_repository_uri=$standard_uri" + echo "standard_repository_arn=$standard_arn" + echo "all_in_one_repository_uri=$all_in_one_uri" + echo "all_in_one_repository_arn=$all_in_one_arn" + } >> "$GITHUB_OUTPUT" + + { + echo "### ${TARGET_ENV^}" + echo "- account: $ACCOUNT_ID" + echo "- region: $AWS_REGION" + echo "- standard repository: $standard_uri" + echo "- standard repository ARN: $standard_arn" + echo "- all-in-one repository: $all_in_one_uri" + echo "- all-in-one repository ARN: $all_in_one_arn" + } >> "$GITHUB_STEP_SUMMARY" + + bootstrap-production: + name: Bootstrap Production ECR + runs-on: ubuntu-latest + env: + ACCOUNT_ID: "877995958936" + ROLE_ARN: ${{ inputs.production_role_arn }} + TARGET_ENV: production + outputs: + standard-repository-uri: ${{ steps.bootstrap.outputs.standard_repository_uri }} + standard-repository-arn: ${{ steps.bootstrap.outputs.standard_repository_arn }} + all-in-one-repository-uri: ${{ steps.bootstrap.outputs.all_in_one_repository_uri }} + all-in-one-repository-arn: ${{ steps.bootstrap.outputs.all_in_one_repository_arn }} + steps: + - name: Configure AWS Credentials + uses: hashicorp/doormat-action@690d0244e8d4751d3090f6785acac1b4b84d5223 + with: + aws-role-arn: ${{ env.ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Bootstrap Repositories + id: bootstrap + shell: bash + run: | + set -euo pipefail + + bootstrap_repository() { + local repository_name="$1" + + if ! aws ecr describe-repositories --region "$AWS_REGION" --repository-names "$repository_name" >/dev/null 2>&1; then + aws ecr create-repository \ + --region "$AWS_REGION" \ + --repository-name "$repository_name" \ + --image-scanning-configuration scanOnPush=true \ + >/dev/null + fi + + aws ecr describe-repositories \ + --region "$AWS_REGION" \ + --repository-names "$repository_name" \ + --query 'repositories[0].[repositoryUri,repositoryArn]' \ + --output text + } + + read -r standard_uri standard_arn < <(bootstrap_repository "$ECR_STANDARD_REPOSITORY") + read -r all_in_one_uri all_in_one_arn < <(bootstrap_repository "$ECR_ALL_IN_ONE_REPOSITORY") + + { + echo "standard_repository_uri=$standard_uri" + echo "standard_repository_arn=$standard_arn" + echo "all_in_one_repository_uri=$all_in_one_uri" + echo "all_in_one_repository_arn=$all_in_one_arn" + } >> "$GITHUB_OUTPUT" + + { + echo "### ${TARGET_ENV^}" + echo "- account: $ACCOUNT_ID" + echo "- region: $AWS_REGION" + echo "- standard repository: $standard_uri" + echo "- standard repository ARN: $standard_arn" + echo "- all-in-one repository: $all_in_one_uri" + echo "- all-in-one repository ARN: $all_in_one_arn" + } >> "$GITHUB_STEP_SUMMARY" + + summary: + name: Summarize ECR Repositories + runs-on: ubuntu-latest + needs: [ bootstrap-integration, bootstrap-production ] + steps: + - name: Summarize Bootstrap Results + shell: bash + run: | + { + echo "## ECR bootstrap complete" + echo + echo "### Integration" + echo "- standard repository: ${{ needs.bootstrap-integration.outputs.standard-repository-uri }}" + echo "- standard repository ARN: ${{ needs.bootstrap-integration.outputs.standard-repository-arn }}" + echo "- all-in-one repository: ${{ needs.bootstrap-integration.outputs.all-in-one-repository-uri }}" + echo "- all-in-one repository ARN: ${{ needs.bootstrap-integration.outputs.all-in-one-repository-arn }}" + echo + echo "### Production" + echo "- standard repository: ${{ needs.bootstrap-production.outputs.standard-repository-uri }}" + echo "- standard repository ARN: ${{ needs.bootstrap-production.outputs.standard-repository-arn }}" + echo "- all-in-one repository: ${{ needs.bootstrap-production.outputs.all-in-one-repository-uri }}" + echo "- all-in-one repository ARN: ${{ needs.bootstrap-production.outputs.all-in-one-repository-arn }}" + } >> "$GITHUB_STEP_SUMMARY" + + echo "Integration standard repository: ${{ needs.bootstrap-integration.outputs.standard-repository-uri }}" + echo "Integration all-in-one repository: ${{ needs.bootstrap-integration.outputs.all-in-one-repository-uri }}" + echo "Production standard repository: ${{ needs.bootstrap-production.outputs.standard-repository-uri }}" + echo "Production all-in-one repository: ${{ needs.bootstrap-production.outputs.all-in-one-repository-uri }}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 860a01151a4e..5a65d05ca56b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,12 +2,27 @@ name: Build env: GO_VERSION: "1.25.8" + AWS_REGION: "us-east-2" + ECR_INTEGRATION_ACCOUNT_ID: "995429640676" + ECR_PRODUCTION_ACCOUNT_ID: "877995958936" + ECR_INTEGRATION_ROLE_ARN: ${{ inputs.integration_role_arn }} + ECR_PRODUCTION_ROLE_ARN: ${{ inputs.production_role_arn }} + ECR_STANDARD_REPOSITORY: "casdoor" + ECR_ALL_IN_ONE_REPOSITORY: "casdoor-all-in-one" on: - push: - branches: - - master - pull_request: + workflow_dispatch: + inputs: + integration_role_arn: + description: Integration AWS role ARN used by Doormat + type: string + required: false + default: arn:aws:iam::995429640676:role/DoormatGithubActionsRole + production_role_arn: + description: Production AWS role ARN used by Doormat + type: string + required: false + default: arn:aws:iam::877995958936:role/DoormatAdminRole jobs: go-tests: @@ -21,7 +36,8 @@ jobs: MYSQL_ROOT_PASSWORD: 123456 ports: - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + options: --health-cmd="mysqladmin ping" --health-interval=10s + --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 @@ -36,7 +52,7 @@ jobs: frontend: name: Front-end runs-on: ubuntu-latest - needs: [go-tests] + needs: [ go-tests ] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -47,7 +63,7 @@ jobs: - run: yarn install && CI=false yarn run build working-directory: ./web - name: Upload build artifacts - if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' + if: github.repository == 'hashicorp-forge/casdoor' uses: actions/upload-artifact@v4 with: name: frontend-build-${{ github.run_id }} @@ -56,7 +72,7 @@ jobs: backend: name: Back-end runs-on: ubuntu-latest - needs: [go-tests] + needs: [ go-tests ] steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 @@ -72,7 +88,7 @@ jobs: linter: name: Go-Linter runs-on: ubuntu-latest - needs: [go-tests] + needs: [ go-tests ] steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 @@ -93,7 +109,7 @@ jobs: e2e: name: e2e-test runs-on: ubuntu-latest - needs: [go-tests] + needs: [ go-tests ] services: mysql: image: mysql:5.7 @@ -102,7 +118,8 @@ jobs: MYSQL_ROOT_PASSWORD: 123456 ports: - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + options: --health-cmd="mysqladmin ping" --health-interval=10s + --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 @@ -161,8 +178,8 @@ jobs: permissions: contents: write issues: write - if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' - needs: [frontend, backend, linter, e2e] + if: github.repository == 'hashicorp-forge/casdoor' + needs: [ frontend, backend, linter, e2e ] outputs: new-release-published: ${{ steps.semantic.outputs.new_release_published }} new-release-version: ${{ steps.semantic.outputs.new_release_version }} @@ -182,8 +199,9 @@ jobs: permissions: contents: write issues: write - if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true' - needs: [tag-release] + if: github.repository == 'hashicorp-forge/casdoor' && + needs.tag-release.outputs.new-release-published == 'true' + needs: [ tag-release ] steps: - name: Checkout uses: actions/checkout@v4 @@ -230,15 +248,16 @@ jobs: name: Docker Release runs-on: ubuntu-latest permissions: - contents: write - issues: write - if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true' - needs: [tag-release] + contents: read + id-token: write + if: github.repository == 'hashicorp-forge/casdoor' && + needs.tag-release.outputs.new-release-published == 'true' + needs: [ tag-release ] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: - fetch-depth: -1 + fetch-depth: 0 - name: Fetch Previous version id: get-previous-tag @@ -246,85 +265,95 @@ jobs: - name: Decide Should_Push Or Not id: should_push + shell: bash run: | - old_version=${{steps.get-previous-tag.outputs.tag}} - new_version=${{ needs.tag-release.outputs.new-release-version }} - - old_array=(${old_version//\./ }) - new_array=(${new_version//\./ }) - - if [ ${old_array[0]} != ${new_array[0]} ] - then - echo ::set-output name=push::'true' - elif [ ${old_array[1]} != ${new_array[1]} ] - then - echo ::set-output name=push::'true' + set -euo pipefail + + old_version='${{ steps.get-previous-tag.outputs.tag }}' + new_version='${{ needs.tag-release.outputs.new-release-version }}' + + old_version="${old_version#v}" + new_version="${new_version#v}" + + if [[ -z "$old_version" ]]; then + echo "push=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + IFS='.' read -r -a old_array <<< "$old_version" + IFS='.' read -r -a new_array <<< "$new_version" + + if [[ "${old_array[0]}" != "${new_array[0]}" ]]; then + echo "push=true" >> "$GITHUB_OUTPUT" + elif [[ "${old_array[1]}" != "${new_array[1]}" ]]; then + echo "push=true" >> "$GITHUB_OUTPUT" else - echo ::set-output name=push::'false' + echo "push=false" >> "$GITHUB_OUTPUT" fi - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 + if: steps.should_push.outputs.push == 'true' - name: Set up buildx id: buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: version: latest + if: steps.should_push.outputs.push == 'true' + + - name: Configure Integration AWS Credentials + if: steps.should_push.outputs.push == 'true' + uses: hashicorp/doormat-action@690d0244e8d4751d3090f6785acac1b4b84d5223 + with: + aws-role-arn: ${{ env.ECR_INTEGRATION_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Log in to Integration ECR + if: steps.should_push.outputs.push == 'true' + shell: bash + run: | + set -euo pipefail + aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "${ECR_INTEGRATION_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" - - name: Log in to Docker Hub - uses: docker/login-action@v1 - if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true' + - name: Configure Production AWS Credentials + if: steps.should_push.outputs.push == 'true' + uses: hashicorp/doormat-action@690d0244e8d4751d3090f6785acac1b4b84d5223 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} + aws-role-arn: ${{ env.ECR_PRODUCTION_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} - - name: Push to Docker Hub - uses: docker/build-push-action@v3 - if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true' + - name: Log in to Production ECR + if: steps.should_push.outputs.push == 'true' + shell: bash + run: | + set -euo pipefail + aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "${ECR_PRODUCTION_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" + + - name: Push Standard Image to ECR + uses: docker/build-push-action@v6 + if: steps.should_push.outputs.push == 'true' with: context: . target: STANDARD platforms: linux/amd64,linux/arm64 push: true - tags: casbin/casdoor:${{ needs.tag-release.outputs.new-release-version }},casbin/casdoor:latest - - - name: Push All In One Version to Docker Hub - uses: docker/build-push-action@v3 - if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true' + tags: | + ${{ env.ECR_INTEGRATION_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_STANDARD_REPOSITORY }}:${{ needs.tag-release.outputs.new-release-version }} + ${{ env.ECR_INTEGRATION_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_STANDARD_REPOSITORY }}:latest + ${{ env.ECR_PRODUCTION_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_STANDARD_REPOSITORY }}:${{ needs.tag-release.outputs.new-release-version }} + ${{ env.ECR_PRODUCTION_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_STANDARD_REPOSITORY }}:latest + + - name: Push All In One Image to ECR + uses: docker/build-push-action@v6 + if: steps.should_push.outputs.push == 'true' with: context: . target: ALLINONE platforms: linux/amd64,linux/arm64 push: true - tags: casbin/casdoor-all-in-one:${{ needs.tag-release.outputs.new-release-version }},casbin/casdoor-all-in-one:latest - - - uses: actions/checkout@v3 - if: steps.should_push.outputs.push=='true' - with: - repository: casdoor/casdoor-helm - ref: "master" - token: ${{ secrets.GH_BOT_TOKEN }} - - - name: Update Helm Chart - if: steps.should_push.outputs.push=='true' - run: | - # Set the appVersion and version of the chart to the current tag - sed -i "s/appVersion: .*/appVersion: ${{ needs.tag-release.outputs.new-release-version }}/g" ./charts/casdoor/Chart.yaml - sed -i "s/version: .*/version: ${{ needs.tag-release.outputs.new-release-version }}/g" ./charts/casdoor/Chart.yaml - - REGISTRY=oci://registry-1.docker.io/casbin - cd charts/casdoor - helm package . - PKG_NAME=$(ls *.tgz) - helm repo index . --url $REGISTRY --merge index.yaml - helm push $PKG_NAME $REGISTRY - rm $PKG_NAME - - # Commit and push the changes back to the repository - git config --global user.name "casbin-bot" - git config --global user.email "bot@casbin.org" - git add Chart.yaml index.yaml - git commit -m "chore(helm): bump helm charts appVersion to ${{ needs.tag-release.outputs.new-release-version }}" - git tag ${{ needs.tag-release.outputs.new-release-version }} - git push origin HEAD:master --follow-tags + tags: | + ${{ env.ECR_INTEGRATION_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_ALL_IN_ONE_REPOSITORY }}:${{ needs.tag-release.outputs.new-release-version }} + ${{ env.ECR_INTEGRATION_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_ALL_IN_ONE_REPOSITORY }}:latest + ${{ env.ECR_PRODUCTION_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_ALL_IN_ONE_REPOSITORY }}:${{ needs.tag-release.outputs.new-release-version }} + ${{ env.ECR_PRODUCTION_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_ALL_IN_ONE_REPOSITORY }}:latest diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 12199177eccf..f8aadb19e15c 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -1,56 +1,61 @@ name: Crowdin Action on: - push: - branches: [ master ] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +env: + CROWDIN_PROJECT_ID: "892410" jobs: synchronize-with-crowdin: runs-on: ubuntu-latest - if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: crowdin action - uses: crowdin/github-action@1.4.8 - with: - upload_translations: true - - download_translations: true - push_translations: true - commit_message: 'refactor: New Crowdin translations by Github Action' - - localization_branch_name: l10n_crowdin_action - create_pull_request: true - pull_request_title: 'refactor: New Crowdin translations' - - crowdin_branch_name: l10n_branch - config: './web/crowdin.yml' - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PROJECT_ID: '463556' - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - - name: crowdin backend action - uses: crowdin/github-action@1.4.8 - with: - upload_translations: true - - download_translations: true - push_translations: true - commit_message: 'refactor: New Crowdin Backend translations by Github Action' - - localization_branch_name: l10n_crowdin_action - create_pull_request: true - pull_request_title: 'refactor: New Crowdin Backend translations' - - crowdin_branch_name: l10n_branch - config: './crowdin.yml' - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PROJECT_ID: '463556' - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + - name: Checkout + uses: actions/checkout@v4 + + - name: crowdin action + uses: crowdin/github-action@v2 + with: + upload_sources: true + upload_translations: true + download_translations: true + push_translations: true + commit_message: "refactor: New Crowdin translations by Github Action" + + localization_branch_name: l10n_crowdin_action + create_pull_request: true + pull_request_title: "refactor: New Crowdin translations" + pull_request_base_branch_name: "master" + + crowdin_branch_name: l10n_branch + config: "./web/crowdin.yml" + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + + - name: crowdin backend action + uses: crowdin/github-action@v2 + with: + upload_sources: true + upload_translations: true + download_translations: true + push_translations: true + commit_message: "refactor: New Crowdin Backend translations by Github Action" + + localization_branch_name: l10n_crowdin_action + create_pull_request: true + pull_request_title: "refactor: New Crowdin Backend translations" + pull_request_base_branch_name: "master" + + crowdin_branch_name: l10n_branch + config: "./crowdin.yml" + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/README.md b/README.md index f055c92cd4cb..76ac14f30d1b 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,6 @@ Discord - - Crowdin -

@@ -81,6 +78,7 @@ --- + ## Why Casdoor Casdoor is a **UI-first** identity provider and access management platform: one place to manage users, organizations, applications, and providers, with a modern web console. Authorization policies can be expressed with **[Casbin](https://casbin.org/)** (ACL, RBAC, ABAC, and more). Unlike reverse-proxy-centric auth companions, Casdoor is a dedicated auth server with broad protocol support, designed to be straightforward to self-host and integrate—see **[casdoor.ai](https://casdoor.ai)** for documentation. @@ -88,18 +86,20 @@ Casdoor is a **UI-first** identity provider and access management platform: one --- + ## 🌐 Live demos -| Environment | URL | Description | -|-------------|-----|-------------| +| Environment | URL | Description | +| ------------- | -------------------------------------------- | --------------------------------------------------------------------------- | | **Read-only** | [door.casdoor.com](https://door.casdoor.com) | Global demo; **any modification or write operation will fail** (read-only). | -| **Writable** | [demo.casdoor.com](https://demo.casdoor.com) | Full access for testing; **data is reset about every 5 minutes**. | +| **Writable** | [demo.casdoor.com](https://demo.casdoor.com) | Full access for testing; **data is reset about every 5 minutes**. | Default demo admin login (where applicable): `admin` / `123` — use only for demos; change credentials on your own deployment. --- + ## 🚀 Quick start Pick one deployment method below. To keep behavior consistent with upstream, the steps are aligned with official docs. @@ -164,6 +164,7 @@ Official guide: [Try with Helm](https://casdoor.ai/docs/basic/try-with-helm) --- + ## ✨ Features @@ -222,6 +223,7 @@ Official guide: [Try with Helm](https://casdoor.ai/docs/basic/try-with-helm) --- + ## Technology stack Casdoor is built as a **frontend–backend separated** project: @@ -234,6 +236,7 @@ Casdoor is built as a **frontend–backend separated** project: --- + ## 📖 Documentation **All product documentation, installation, and tutorials live at [casdoor.ai/docs/overview](https://casdoor.ai/docs/overview).** Start here, then use the sections below. @@ -256,6 +259,7 @@ Casdoor is built as a **frontend–backend separated** project: --- + ## 🔌 Integrations Casdoor integrates with common languages and frameworks: @@ -275,6 +279,7 @@ Browse the full list: [Integrations](https://casdoor.ai/docs/category/integratio --- + ## 🤝 Community and support - **Discord**: [Join our community](https://discord.gg/5rPsrAzK7S) @@ -285,6 +290,7 @@ Browse the full list: [Integrations](https://casdoor.ai/docs/category/integratio --- + ## 🌍 Contributing If you have questions about Casdoor, you can **[open an issue](https://github.com/casdoor/casdoor/issues)**. Pull requests are welcome; **we recommend opening an issue first** so you can align with maintainers and the community before larger changes. @@ -293,12 +299,13 @@ Please also read our [contribution guidelines](https://casdoor.ai/docs/contribut ### Translation and i18n -- **Crowdin** is used for translation workflows: [casdoor-site on Crowdin](https://crowdin.com/project/casdoor-site). +- **Crowdin** is used for translation workflows: [hcp-casdoor on Crowdin](https://crowdin.com/project/hcp-casdoor). - The web app uses **i18next**. When you add or change user-visible strings under [`web/`](https://github.com/casdoor/casdoor/tree/master/web), update the English catalog at [`web/src/locales/en/data.json`](web/src/locales/en/data.json) accordingly. --- + ## 📄 License Casdoor is licensed under the [Apache License 2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE). diff --git a/crowdin.yml b/crowdin.yml index dbcc79710af9..fc070065cccb 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,4 +1,4 @@ -project_id: '491513' +project_id_env: 'CROWDIN_PROJECT_ID' api_token_env: 'CROWDIN_PERSONAL_TOKEN' preserve_hierarchy: true files: [ diff --git a/docs/docker-build.md b/docs/docker-build.md new file mode 100644 index 000000000000..6a7f6fc8ac80 --- /dev/null +++ b/docs/docker-build.md @@ -0,0 +1,141 @@ +# Docker build guide — HCP Casdoor + +This document describes how to build the project's Docker images locally (both the standard runtime image and the "all-in-one" image), how to run them for local testing, and how to push them to a registry (ECR) if desired. + +Files of interest + +- Dockerfile: [Dockerfile](Dockerfile) +- CI build workflow: [.github/workflows/build.yml](.github/workflows/build.yml) + +Overview + +- The repository Dockerfile is a multi-stage build that defines these notable stages: + - `FRONT` — builds the frontend under `/web` (Node/Yarn). + - `BACK` — builds the Go backend and runs `./build.sh` to produce server artifacts. + - `STANDARD` — minimal Alpine runtime image containing the server binary + web build. + - `ALLINONE` — Debian-based image that bundles server, web build and entrypoint for quick trials. + +Prerequisites + +- Docker Engine (Docker Desktop on macOS/Windows or Docker on Linux). +- Docker Buildx (modern Docker includes buildx; Docker Desktop exposes it). +- For pushing to ECR: the AWS CLI configured locally with credentials that have ECR permissions, or other valid login method. + +Quick local setup (one-time) + +1. Create/use a buildx builder (recommended): + +```bash +docker buildx create --name mybuilder --use || docker buildx use mybuilder +docker buildx inspect --bootstrap +``` + +2. (Optional) Ensure BuildKit is enabled if you rely on it: + +```bash +export DOCKER_BUILDKIT=1 +``` + +Build the ALL-IN-ONE image (single-platform, load into local daemon) + +This is the same `ALLINONE` target used in CI. + +```bash +docker buildx build --platform linux/amd64 --target ALLINONE --load \ + --pull --progress=plain \ + -t casdoor-all-in-one:local . +``` + +Notes: + +- `--load` places the resulting image into your local Docker daemon (works for a single platform only). +- `--pull` refreshes base images before building. +- `--progress=plain` gives more readable logs for debugging build failures. + +Fallback (simpler) build (non-buildx) + +If you're running on a Linux host that matches the target platform, you can use the classic build command: + +```bash +docker build --target ALLINONE -t casdoor-all-in-one:local . +``` + +Build the STANDARD runtime image (single-platform) + +```bash +docker buildx build --platform linux/amd64 --target STANDARD --load -t casdoor:local . +``` + +Run the image locally + +```bash +docker run --rm -it -p 8000:8000 casdoor-all-in-one:local +``` + +The `ALLINONE` image's `ENTRYPOINT`/`CMD` execute `/docker-entrypoint.sh` which starts the server (the image exposes the service on port 8000 by default). Use `docker logs ` if it exits immediately to see the startup errors. + +Mount local config or persist logs + +To substitute the configuration file or persist logs, mount host paths into the container: + +```bash +docker run --rm -it -p 8000:8000 \ + -v "$PWD/conf/app.conf":/conf/app.conf \ + -v "$PWD/logs":/logs \ + casdoor-all-in-one:local +``` + +Build multi-architecture images and push to a registry + +To build multi-arch and push to a remote registry (replace `` with your target): + +```bash +docker buildx build --platform linux/amd64,linux/arm64 --target ALLINONE \ + --push -t /casdoor-all-in-one:latest . +``` + +Pushing to AWS ECR (example) + +Local push to Amazon ECR requires valid AWS credentials locally. Example (replace `` and region if needed): + +```bash +# login +aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin .dkr.ecr.us-east-2.amazonaws.com + +# tag and push (single-platform) +docker tag casdoor-all-in-one:local .dkr.ecr.us-east-2.amazonaws.com/casdoor-all-in-one:latest +docker push .dkr.ecr.us-east-2.amazonaws.com/casdoor-all-in-one:latest + +# or buildx direct push for multi-arch +docker buildx build --platform linux/amd64,linux/arm64 --target ALLINONE \ + --push -t .dkr.ecr.us-east-2.amazonaws.com/casdoor-all-in-one:latest . +``` + +Note: In CI the repository uses Doormat to obtain AWS credentials and then `docker/build-push-action` to publish images. Locally you will either need `aws configure` credentials or another method to authenticate to ECR. + +Verification and debug commands + +```bash +docker images | grep casdoor +docker ps -a +docker logs +docker inspect +``` + +Common problems & tips + +- Build is slow: enable caching or keep a persistent buildx builder (`docker buildx create --use`) to reuse layers. +- Platform mismatch: use `--platform linux/amd64` for x86_64 builds on macOS/Apple Silicon (via emulation) or build native arm64 if you need it. +- Entrypoint/script errors: inspect `/docker-entrypoint.sh` inside the image to verify permissions and content. +- Not enough disk space: remove dangling images `docker system prune -af` carefully. + +Why the Dockerfile works as-is + +- The `BACK` stage runs `./build.sh` during the build to compile the server. That means you do not need to pre-build the binary on the host; the container build runs the compile steps inside the builder stage. + +See also + +- The project Dockerfile: [Dockerfile](Dockerfile) +- The CI build & release flow that uses the same targets: [.github/workflows/build.yml](.github/workflows/build.yml) + +If you want, I can run a local build on your machine now (it will use your Docker daemon). Say "yes" and I will proceed, or tell me which image/target and platform you want built. diff --git a/docs/hcp-casdoor-setup.md b/docs/hcp-casdoor-setup.md new file mode 100644 index 000000000000..1efbd6ad9211 --- /dev/null +++ b/docs/hcp-casdoor-setup.md @@ -0,0 +1,266 @@ +# HCP Casdoor Setup Guide + +This guide walks through the full setup for the forked repository: + +- Crowdin translation sync +- AWS ECR bootstrap in integration and production +- Manual build and release publishing to ECR + +It assumes the repository is `hashicorp-forge/casdoor`, the Crowdin project is `hcp-casdoor`, and the AWS region is `us-east-2`. + +## Quick Reference + +| Item | Value | +| -------------------- | --------------------------------------------------------- | +| Crowdin project name | `hcp-casdoor` | +| Crowdin project ID | `892410` | +| Crowdin project URL | `https://crowdin.com/project/hcp-casdoor` | +| Crowdin project type | File-based | +| Crowdin visibility | Private | +| Source language | English | +| AWS region | `us-east-2` | +| Integration account | `995429640676` | +| Integration role ARN | `arn:aws:iam::995429640676:role/DoormatGithubActionsRole` | +| Production account | `877995958936` | +| Production role ARN | `arn:aws:iam::877995958936:role/DoormatAdminRole` | +| ECR repositories | `casdoor`, `casdoor-all-in-one` | + +## What Is Already Configured + +The repository already contains these workflows and files: + +- `.github/workflows/sync.yml` for Crowdin sync +- `.github/workflows/bootstrap-ecr.yml` for ECR repository bootstrap +- `.github/workflows/build.yml` for manual build and release publishing +- `crowdin.yml` for backend translation files +- `web/crowdin.yml` for frontend translation files + +The Crowdin configs read the project ID from `CROWDIN_PROJECT_ID`, so the GitHub workflow supplies the value instead of hardcoding it in two places. + +## Prerequisites + +Before you run anything, make sure you have: + +1. Admin or maintainer access to the GitHub repository. +2. A Crowdin account and a private file-based project named `hcp-casdoor`. +3. Access to create and manage GitHub repository secrets. +4. Access to run GitHub Actions workflows in the repository. + +You do not need AWS access keys in GitHub secrets. The workflows use Doormat to obtain AWS credentials at runtime. + +## 1. Create or Verify the Crowdin Project + +1. Log in to Crowdin and open the project `hcp-casdoor`. +2. Make the project private. +3. Choose a file-based project. +4. Set the source language to English. +5. Select the initial target languages you want to support. +6. Skip the onboarding app unless you want the extra guided setup experience. +7. Confirm the project ID is `892410`. +8. Confirm the project URL is `https://crowdin.com/project/hcp-casdoor`. + +Notes: + +- Selecting the top 30 languages is fine to start. +- You can add more target languages later. +- One shared Crowdin project is the correct choice here because both the frontend and backend translation files point to the same project ID. + +## 2. Add the Required GitHub Secret + +Add this repository secret in GitHub: + +- `CROWDIN_PERSONAL_TOKEN` + +This token lets the Crowdin GitHub Action talk to Crowdin. Keep it in GitHub Secrets only. Do not store it in the repository. + +You do not need to add these as repository secrets: + +- `CROWDIN_PROJECT_ID` because the workflow sets it to `892410` +- `GITHUB_TOKEN` because GitHub provides it automatically to workflows +- AWS access keys because Doormat handles AWS authentication + +## 3. Set the GitHub Actions Permissions + +The Crowdin workflow creates pull requests automatically. To allow that, check the repository settings: + +1. Open the repository settings. +2. Go to the Actions settings page. +3. Make sure workflow permissions allow write access. +4. Allow GitHub Actions to create and approve pull requests if your repository policy requires it. + +The Crowdin workflow uses the built-in `GITHUB_TOKEN`, so no personal access token is needed for GitHub itself in the current setup. + +## 4. Run the Crowdin Sync Workflow + +The Crowdin workflow is manual now. + +1. Open the repository Actions tab. +2. Select `Crowdin Action`. +3. Run the workflow with `workflow_dispatch`. +4. Confirm that the workflow uses the repository secret `CROWDIN_PERSONAL_TOKEN`. +5. Confirm that it creates or updates a pull request into `master`. + +What the workflow does: + +- Uploads the backend source files from `crowdin.yml`. +- Uploads the frontend source files from `web/crowdin.yml`. +- Downloads translations. +- Pushes the translation changes to the localization branch. +- Opens a pull request automatically. + +If the workflow fails, check these items first: + +- The Crowdin project ID is still `892410`. +- The `CROWDIN_PERSONAL_TOKEN` secret exists. +- Repository Actions permissions allow pull request creation. +- The project is private and file-based. + +## 5. Bootstrap ECR in Both AWS Accounts + +The ECR bootstrap workflow creates the repositories in the two AWS accounts if they do not already exist. + +1. Open the repository Actions tab. +2. Select `Bootstrap ECR`. +3. Run the workflow with `workflow_dispatch`. +4. Keep the default role ARNs unless you intentionally changed them. +5. Confirm the workflow runs in `us-east-2`. +6. Review the job summary for the repository URIs and ARNs. + +The workflow creates these repositories in both accounts: + +- `casdoor` +- `casdoor-all-in-one` + +The integration account and default role are: + +- Account: `995429640676` +- Role ARN: `arn:aws:iam::995429640676:role/DoormatGithubActionsRole` + +The production account and default role are: + +- Account: `877995958936` +- Role ARN: `arn:aws:iam::877995958936:role/DoormatAdminRole` + +No AWS secret is needed for this workflow. The workflow uses the pinned Doormat GitHub Action to assume the AWS role and export temporary credentials for the job. + +If you need to verify the result, open AWS and confirm that each account has: + +- `casdoor` +- `casdoor-all-in-one` + +## 6. Run the Manual Build and Release Workflow + +The `Build` workflow is also manual. + +1. Open the repository Actions tab. +2. Select `Build`. +3. Run the workflow with `workflow_dispatch`. +4. Leave the role ARN inputs at their defaults unless the AWS roles changed. +5. Review the job output after the run finishes. + +The workflow does the following: + +- Runs tests and the backend/frontend build steps. +- Runs semantic release. +- Assumes the integration AWS role and production AWS role through Doormat. +- Logs in to both ECR registries. +- Pushes the standard image and the all-in-one image to both accounts. + +Important note: + +- The Docker publish steps only run when semantic release reports a new release. +- If no new release is detected, the publish jobs skip. + +## 7. Understand the Crowdin Authentication Choice + +This repository uses the built-in `GITHUB_TOKEN` for Crowdin pull request creation. + +### Why this is the simplest option + +- It is created automatically by GitHub. +- It does not require another long-lived secret. +- It is scoped to the repository. +- It is the lowest-friction setup for same-repo pull requests. + +### Tradeoffs + +- It only works inside the repository workflow context. +- It depends on repository Actions permissions. +- It is not ideal if you need a stable human identity on commits. + +### When to use other options later + +- Use a personal access token if you need a human identity or cross-repo access. +- Use a GitHub App token if you want tighter least-privilege controls and are willing to spend more time on setup. + +### Current setup steps for `GITHUB_TOKEN` + +1. Keep `permissions: contents: write` and `pull-requests: write` in the Crowdin workflow. +2. Leave `env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}` in the workflow. +3. Make sure repository settings allow GitHub Actions to create pull requests. + +## 8. Keep the Public Links Updated + +The repository now points at the new Crowdin project URL: + +- `https://crowdin.com/project/hcp-casdoor` + +The README no longer shows the Crowdin badge. The web app still links to the Crowdin project from the translation notice. + +If the Crowdin project ever changes again, update these locations: + +- README project link +- Web app Crowdin link +- Crowdin workflow project ID + +## 9. Final Setup Checklist + +Use this checklist to confirm everything is ready: + +- [ ] Crowdin project `hcp-casdoor` exists and is private +- [ ] Crowdin project ID is `892410` +- [ ] Crowdin is file-based, not string-based +- [ ] `CROWDIN_PERSONAL_TOKEN` is stored as a GitHub secret +- [ ] Repository Actions permissions allow PR creation +- [ ] `Bootstrap ECR` has been run successfully +- [ ] `Build` has been run successfully +- [ ] README and the web app point to `hcp-casdoor` on Crowdin +- [ ] No AWS access keys were added to GitHub secrets + +## 10. Troubleshooting + +### Crowdin PR is not created + +Check the following: + +- `CROWDIN_PERSONAL_TOKEN` exists and is correct. +- GitHub Actions permissions allow write access. +- GitHub Actions can create pull requests in the repository settings. +- The workflow is running in the right repository. + +### Crowdin sync uses the wrong project + +Check the following: + +- `CROWDIN_PROJECT_ID` is still `892410` in the workflow. +- `crowdin.yml` and `web/crowdin.yml` still use `project_id_env`. + +### ECR bootstrap fails + +Check the following: + +- The workflow inputs still point to the correct role ARNs. +- The AWS role trust policy allows Doormat to assume the role. +- The workflow is running in `us-east-2`. + +### Build publishes nothing + +Check the following: + +- Semantic release detected a new release. +- The release job completed successfully before the Docker job. +- The ECR bootstrap workflow already created the repositories. + +### You want to change the GitHub token approach later + +You can switch to a personal access token or GitHub App token later if the repository policy changes. For now, `GITHUB_TOKEN` is the easiest path and keeps the setup lightweight. diff --git a/object/saml_idp.go b/object/saml_idp.go index cd9ee28c44e0..48e4a33eab6c 100644 --- a/object/saml_idp.go +++ b/object/saml_idp.go @@ -162,6 +162,23 @@ func (x X509Key) GetKeyPair() (privateKey *rsa.PrivateKey, cert []byte, err erro return privateKey, cert, err } +func newSamlSigningContext(application *Application, keyStore dsig.X509KeyStore) *dsig.SigningContext { + ctx := dsig.NewDefaultSigningContext(keyStore) + if application.SamlHashAlgorithm == "" || application.SamlHashAlgorithm == "SHA1" { + ctx.Hash = crypto.SHA1 + } else if application.SamlHashAlgorithm == "SHA256" { + ctx.Hash = crypto.SHA256 + } else if application.SamlHashAlgorithm == "SHA512" { + ctx.Hash = crypto.SHA512 + } + + if application.EnableSamlC14n10 { + ctx.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("") + } + + return ctx +} + // IdpEntityDescriptor // SAML METADATA type IdpEntityDescriptor struct { @@ -375,18 +392,7 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h PrivateKey: cert.PrivateKey, X509Certificate: certificate, } - ctx := dsig.NewDefaultSigningContext(randomKeyStore) - if application.SamlHashAlgorithm == "" || application.SamlHashAlgorithm == "SHA1" { - ctx.Hash = crypto.SHA1 - } else if application.SamlHashAlgorithm == "SHA256" { - ctx.Hash = crypto.SHA256 - } else if application.SamlHashAlgorithm == "SHA512" { - ctx.Hash = crypto.SHA512 - } - - if application.EnableSamlC14n10 { - ctx.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("xs") - } + ctx := newSamlSigningContext(application, randomKeyStore) // signedXML, err := ctx.SignEnvelopedLimix(samlResponse) // if err != nil { diff --git a/object/saml_idp_test.go b/object/saml_idp_test.go new file mode 100644 index 000000000000..456c7d383ef1 --- /dev/null +++ b/object/saml_idp_test.go @@ -0,0 +1,140 @@ +// Copyright 2026 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "math/big" + "strings" + "testing" + "time" + + "github.com/beevik/etree" +) + +func TestNewSamlSigningContextCanonicalizer(t *testing.T) { + keyStore := newSamlSigningTestKeyStore(t) + + tests := []struct { + name string + enableSamlC14n10 bool + wantAlgorithm string + unwantedAlgorithm string + wantPrefixListXs bool + }{ + { + name: "default branch keeps c14n11", + enableSamlC14n10: false, + wantAlgorithm: "http://www.w3.org/2006/12/xml-c14n11", + unwantedAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + wantPrefixListXs: false, + }, + { + name: "c14n10 branch uses exclusive canonicalization without prefix list", + enableSamlC14n10: true, + wantAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + unwantedAlgorithm: "http://www.w3.org/2006/12/xml-c14n11", + wantPrefixListXs: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := newSamlSigningContext(&Application{EnableSamlC14n10: tt.enableSamlC14n10}, keyStore) + signatureXML := buildSamlSignatureXML(t, ctx) + + if !strings.Contains(signatureXML, tt.wantAlgorithm) { + t.Fatalf("signature = %q, want algorithm %q", signatureXML, tt.wantAlgorithm) + } + + if tt.unwantedAlgorithm != "" && strings.Contains(signatureXML, tt.unwantedAlgorithm) { + t.Fatalf("signature = %q, found unexpected algorithm %q", signatureXML, tt.unwantedAlgorithm) + } + + hasPrefixListXs := strings.Contains(signatureXML, `PrefixList="xs"`) || strings.Contains(signatureXML, "InclusiveNamespaces") + if hasPrefixListXs != tt.wantPrefixListXs { + t.Fatalf("signature = %q, PrefixList xs presence = %v, want %v", signatureXML, hasPrefixListXs, tt.wantPrefixListXs) + } + }) + } +} + +func buildSamlSignatureXML(t *testing.T, ctx interface { + ConstructSignature(el *etree.Element, enveloped bool) (*etree.Element, error) +}) string { + t.Helper() + + assertion := etree.NewElement("saml:Assertion") + assertion.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") + assertion.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema") + assertion.CreateAttr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + assertion.CreateAttr("ID", "_test-assertion") + assertion.CreateElement("saml:Issuer").SetText("https://example.com") + attributeValue := assertion.CreateElement("saml:AttributeStatement").CreateElement("saml:Attribute").CreateElement("saml:AttributeValue") + attributeValue.CreateAttr("xsi:type", "xs:string") + attributeValue.SetText("user@example.com") + + signature, err := ctx.ConstructSignature(assertion, true) + if err != nil { + t.Fatalf("ConstructSignature() error = %v", err) + } + + doc := etree.NewDocument() + doc.SetRoot(signature) + + bytes, err := doc.WriteToBytes() + if err != nil { + t.Fatalf("WriteToBytes() error = %v", err) + } + + return string(bytes) +} + +func newSamlSigningTestKeyStore(t *testing.T) *X509Key { + t.Helper() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("GenerateKey() error = %v", err) + } + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "casdoor.test", + }, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + BasicConstraintsValid: true, + } + + certificateDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey) + if err != nil { + t.Fatalf("CreateCertificate() error = %v", err) + } + + privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) + + return &X509Key{ + PrivateKey: string(privateKeyPEM), + X509Certificate: base64.StdEncoding.EncodeToString(certificateDER), + } +} diff --git a/web/crowdin.yml b/web/crowdin.yml index 3929ddba3b9a..366ea1a7361b 100644 --- a/web/crowdin.yml +++ b/web/crowdin.yml @@ -1,4 +1,4 @@ -project_id: '491513' +project_id_env: 'CROWDIN_PROJECT_ID' api_token_env: 'CROWDIN_PERSONAL_TOKEN' preserve_hierarchy: true files: [ diff --git a/web/package.json b/web/package.json index 5f1de31f48cc..ed3a9708a224 100644 --- a/web/package.json +++ b/web/package.json @@ -54,10 +54,10 @@ "react-highlight-words": "^0.18.0", "react-i18next": "^11.8.7", "react-metamask-avatar": "^1.2.1", - "reactflow": "^11.11.4", "react-router-dom": "^5.3.3", "react-scripts": "5.0.1", "react-social-login-buttons": "^3.4.0", + "reactflow": "^11.11.4", "xlsx": "^0.18.5" }, "scripts": { diff --git a/web/src/App.js b/web/src/App.js index 45d6f9d335b7..11596fbfd400 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -17,9 +17,25 @@ import "./App.less"; import {Helmet} from "react-helmet"; import * as Setting from "./Setting"; import {setOrgIsTourVisible, setTourLogo} from "./TourConfig"; -import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs"; -import {GithubOutlined, InfoCircleFilled, ShareAltOutlined} from "@ant-design/icons"; -import {Alert, Button, ConfigProvider, Drawer, FloatButton, Layout, Result, Tooltip} from "antd"; +import { + StyleProvider, + legacyLogicalPropertiesTransformer +} from "@ant-design/cssinjs"; +import { + GithubOutlined, + InfoCircleFilled, + ShareAltOutlined +} from "@ant-design/icons"; +import { + Alert, + Button, + ConfigProvider, + Drawer, + FloatButton, + Layout, + Result, + Tooltip +} from "antd"; import {AiDots} from "./common/Loading"; import {Route, Switch, withRouter} from "react-router-dom"; import CustomGithubCorner from "./common/CustomGithubCorner"; @@ -72,33 +88,33 @@ setTwoToneColor("rgb(87,52,211)"); function getAntdLocale(language) { const localeMap = { - "en": enUS, - "zh": zhCN, + en: enUS, + zh: zhCN, "zh-tw": zhTW, - "es": esES, - "fr": frFR, - "de": deDE, - "id": idID, - "ja": jaJP, - "ko": koKR, - "ru": ruRU, - "vi": viVN, - "pt": ptBR, - "it": itIT, - "ms": msMY, - "tr": trTR, - "ar": arEG, - "he": heIL, - "nl": nlNL, - "pl": plPL, - "fi": fiFI, - "sv": svSE, - "uk": ukUA, - "fa": faIR, - "cs": csCZ, - "sk": skSK, - "kk": ruRU, // Use Russian for Kazakh as antd doesn't have Kazakh - "az": trTR, // Use Turkish for Azerbaijani as they're similar + es: esES, + fr: frFR, + de: deDE, + id: idID, + ja: jaJP, + ko: koKR, + ru: ruRU, + vi: viVN, + pt: ptBR, + it: itIT, + ms: msMY, + tr: trTR, + ar: arEG, + he: heIL, + nl: nlNL, + pl: plPL, + fi: fiFI, + sv: svSE, + uk: ukUA, + fa: faIR, + cs: csCZ, + sk: skSK, + kk: ruRU, // Use Russian for Kazakh as antd doesn't have Kazakh + az: trTR, // Use Turkish for Azerbaijani as they're similar }; return localeMap[language] || enUS; } @@ -109,7 +125,9 @@ class App extends Component { this.setThemeAlgorithm(); let storageThemeAlgorithm = []; try { - storageThemeAlgorithm = localStorage.getItem("themeAlgorithm") ? JSON.parse(localStorage.getItem("themeAlgorithm")) : ["default"]; + storageThemeAlgorithm = localStorage.getItem("themeAlgorithm") + ? JSON.parse(localStorage.getItem("themeAlgorithm")) + : ["default"]; } catch { storageThemeAlgorithm = ["default"]; } @@ -147,16 +165,24 @@ class App extends Component { } if (this.state.account !== prevState.account) { - const requiredEnableMfa = Setting.isRequiredEnableMfa(this.state.account, this.state.account?.organization); + const requiredEnableMfa = Setting.isRequiredEnableMfa( + this.state.account, + this.state.account?.organization + ); this.setState({ requiredEnableMfa: requiredEnableMfa, }); if (requiredEnableMfa === true) { - const mfaType = Setting.getMfaItemsByRules(this.state.account, this.state.account?.organization, [Setting.MfaRuleRequired]) - .find((item) => item.rule === Setting.MfaRuleRequired)?.name; + const mfaType = Setting.getMfaItemsByRules( + this.state.account, + this.state.account?.organization, + [Setting.MfaRuleRequired] + ).find((item) => item.rule === Setting.MfaRuleRequired)?.name; if (mfaType !== undefined) { - this.props.history.push(`/mfa/setup?mfaType=${mfaType}`, {from: "/login"}); + this.props.history.push(`/mfa/setup?mfaType=${mfaType}`, { + from: "/login", + }); } } } @@ -164,7 +190,9 @@ class App extends Component { shouldFlattenMenu() { const organization = this.state.account?.organization; - const navItems = Setting.isLocalAdminUser(this.state.account) ? organization?.navItems : (organization?.userNavItems ?? []); + const navItems = Setting.isLocalAdminUser(this.state.account) + ? organization?.navItems + : (organization?.userNavItems ?? []); // If navItems is "all" or not configured, don't flatten if (!Array.isArray(navItems) || navItems?.includes("all")) { @@ -174,17 +202,52 @@ class App extends Component { // Count how many valid menu items would be visible // Filter out any invalid or non-existent menu items const validMenuItems = [ - "/", "/shortcuts", "/apps", // Home group - "/organizations", "/groups", "/users", "/invitations", // User Management - "/applications", "/providers", "/resources", "/certs", "/keys", // Identity - "/roles", "/permissions", "/models", "/adapters", "/enforcers", // Authorization - "/agents", "/servers", "/server-store", "/entries", "/sites", "/rules", // LLM AI - "/sessions", "/records", "/tokens", "/verifications", // Auditing - "/products", "/orders", "/payments", "/plans", "/pricings", "/subscriptions", "/transactions", // Business - "/sysinfo", "/forms", "/syncers", "/webhooks", "/webhook-events", "/tickets", "/swagger", // Admin + "/", + "/shortcuts", + "/apps", // Home group + "/organizations", + "/groups", + "/users", + "/invitations", // User Management + "/applications", + "/providers", + "/resources", + "/certs", + "/keys", // Identity + "/roles", + "/permissions", + "/models", + "/adapters", + "/enforcers", // Authorization + "/agents", + "/servers", + "/server-store", + "/entries", + "/sites", + "/rules", // LLM AI + "/sessions", + "/records", + "/tokens", + "/verifications", // Auditing + "/products", + "/orders", + "/payments", + "/plans", + "/pricings", + "/subscriptions", + "/transactions", // Business + "/sysinfo", + "/forms", + "/syncers", + "/webhooks", + "/webhook-events", + "/tickets", + "/swagger", // Admin ]; - const count = navItems.filter(item => validMenuItems.includes(item)).length; + const count = navItems.filter((item) => + validMenuItems.includes(item) + ).length; return count <= Conf.MaxItemsForFlatMenu; } @@ -198,7 +261,13 @@ class App extends Component { } else if (uri.includes("/apps")) { return "/apps"; } - } else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) { + } else if ( + uri.includes("/organizations") || + uri.includes("/trees") || + uri.includes("/groups") || + uri.includes("/users") || + uri.includes("/invitations") + ) { if (uri.includes("/organizations")) { return "/organizations"; } else if (uri.includes("/groups")) { @@ -208,7 +277,12 @@ class App extends Component { } else if (uri.includes("/invitations")) { return "/invitations"; } - } else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs")) { + } else if ( + uri.includes("/applications") || + uri.includes("/providers") || + uri.includes("/resources") || + uri.includes("/certs") + ) { if (uri.includes("/applications")) { return "/applications"; } else if (uri.includes("/providers")) { @@ -220,7 +294,13 @@ class App extends Component { } } else if (uri.includes("/keys")) { return "/keys"; - } else if (uri.includes("/agents") || uri.includes("/servers") || uri.includes("/entries") || uri.includes("/sites") || uri.includes("/rules")) { + } else if ( + uri.includes("/agents") || + uri.includes("/servers") || + uri.includes("/entries") || + uri.includes("/sites") || + uri.includes("/rules") + ) { if (uri.includes("/agents")) { return "/agents"; } else if (uri.includes("/servers")) { @@ -234,7 +314,13 @@ class App extends Component { } else if (uri.includes("/rules")) { return "/rules"; } - } else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) { + } else if ( + uri.includes("/roles") || + uri.includes("/permissions") || + uri.includes("/models") || + uri.includes("/adapters") || + uri.includes("/enforcers") + ) { if (uri.includes("/roles")) { return "/roles"; } else if (uri.includes("/permissions")) { @@ -246,7 +332,12 @@ class App extends Component { } else if (uri.includes("/enforcers")) { return "/enforcers"; } - } else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) { + } else if ( + uri.includes("/records") || + uri.includes("/tokens") || + uri.includes("/sessions") || + uri.includes("/verifications") + ) { if (uri.includes("/sessions")) { return "/sessions"; } else if (uri.includes("/records")) { @@ -256,7 +347,15 @@ class App extends Component { } else if (uri.includes("/verifications")) { return "/verifications"; } - } else if (uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions")) { + } else if ( + uri.includes("/products") || + uri.includes("/orders") || + uri.includes("/payments") || + uri.includes("/plans") || + uri.includes("/pricings") || + uri.includes("/subscriptions") || + uri.includes("/transactions") + ) { if (uri.includes("/products")) { return "/products"; } else if (uri.includes("/orders")) { @@ -272,7 +371,14 @@ class App extends Component { } else if (uri.includes("/transactions")) { return "/transactions"; } - } else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks") || uri.includes("/webhook-events") || uri.includes("/tickets")) { + } else if ( + uri.includes("/sysinfo") || + uri.includes("/forms") || + uri.includes("/syncers") || + uri.includes("/webhooks") || + uri.includes("/webhook-events") || + uri.includes("/tickets") + ) { if (uri.includes("/sysinfo")) { return "/sysinfo"; } else if (uri.includes("/forms")) { @@ -312,19 +418,65 @@ class App extends Component { // Original logic for grouped menu if (uri === "/" || uri.includes("/shortcuts") || uri.includes("/apps")) { this.setState({selectedMenuKey: "/home"}); - } else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) { + } else if ( + uri.includes("/organizations") || + uri.includes("/trees") || + uri.includes("/groups") || + uri.includes("/users") || + uri.includes("/invitations") + ) { this.setState({selectedMenuKey: "/orgs"}); - } else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs") || uri.includes("/keys")) { + } else if ( + uri.includes("/applications") || + uri.includes("/providers") || + uri.includes("/resources") || + uri.includes("/certs") || + uri.includes("/keys") + ) { this.setState({selectedMenuKey: "/identity"}); - } else if (uri.includes("/agents") || uri.includes("/servers") || uri.includes("/server-store") || uri.includes("/entries") || uri.includes("/sites") || uri.includes("/rules")) { + } else if ( + uri.includes("/agents") || + uri.includes("/servers") || + uri.includes("/server-store") || + uri.includes("/entries") || + uri.includes("/sites") || + uri.includes("/rules") + ) { this.setState({selectedMenuKey: "/gateway"}); - } else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) { + } else if ( + uri.includes("/roles") || + uri.includes("/permissions") || + uri.includes("/models") || + uri.includes("/adapters") || + uri.includes("/enforcers") + ) { this.setState({selectedMenuKey: "/auth"}); - } else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) { + } else if ( + uri.includes("/records") || + uri.includes("/tokens") || + uri.includes("/sessions") || + uri.includes("/verifications") + ) { this.setState({selectedMenuKey: "/logs"}); - } else if (uri.includes("/product-store") || uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions")) { + } else if ( + uri.includes("/product-store") || + uri.includes("/products") || + uri.includes("/orders") || + uri.includes("/payments") || + uri.includes("/plans") || + uri.includes("/pricings") || + uri.includes("/subscriptions") || + uri.includes("/transactions") + ) { this.setState({selectedMenuKey: "/business"}); - } else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks") || uri.includes("/webhook-events") || uri.includes("/tickets")) { + } else if ( + uri.includes("/sysinfo") || + uri.includes("/forms") || + uri.includes("/syncers") || + uri.includes("/webhooks") || + uri.includes("/webhook-events") || + uri.includes("/tickets") + ) { this.setState({selectedMenuKey: "/admin"}); } else if (uri.includes("/signup")) { this.setState({selectedMenuKey: "/signup"}); @@ -394,7 +546,9 @@ class App extends Component { if (localStorage.getItem("themeAlgorithm")) { let storageThemeAlgorithm = []; try { - storageThemeAlgorithm = JSON.parse(localStorage.getItem("themeAlgorithm")); + storageThemeAlgorithm = JSON.parse( + localStorage.getItem("themeAlgorithm") + ); } catch { storageThemeAlgorithm = ["default"]; } @@ -416,17 +570,16 @@ class App extends Component { if (!applicationName) { return; } - ApplicationBackend.getApplication("admin", applicationName) - .then((res) => { - if (res.status === "error") { - Setting.showMessage("error", res.msg); - return; - } + ApplicationBackend.getApplication("admin", applicationName).then((res) => { + if (res.status === "error") { + Setting.showMessage("error", res.msg); + return; + } - this.setState({ - application: res.data, - }); + this.setState({ + application: res.data, }); + }); } getAccount() { @@ -439,7 +592,9 @@ class App extends Component { const query2 = this.getLanguageParam(params); if (query2 !== "") { - const url = window.location.toString().replace(new RegExp(`[?&]${query2}`), ""); + const url = window.location + .toString() + .replace(new RegExp(`[?&]${query2}`), ""); window.history.replaceState({}, document.title, url); } @@ -457,32 +612,37 @@ class App extends Component { window.history.replaceState({}, document.title, newUrl); } - AuthBackend.getAccount(query) - .then((res) => { - let account = null; - let accessToken = null; - if (res.status === "ok") { - account = res.data; - account.organization = res.data2; - accessToken = res.data.accessToken; - - if (!localStorage.getItem("language")) { - this.setLanguage(account); - } - this.setTheme(Setting.getThemeData(account.organization), Conf.InitThemeAlgorithm); - setTourLogo(account.organization.logo); - setOrgIsTourVisible(account.organization.enableTour); - } else { - if (res.data !== "Please login first") { - Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`); - } + AuthBackend.getAccount(query).then((res) => { + let account = null; + let accessToken = null; + if (res.status === "ok") { + account = res.data; + account.organization = res.data2; + accessToken = res.data.accessToken; + + if (!localStorage.getItem("language")) { + this.setLanguage(account); } + this.setTheme( + Setting.getThemeData(account.organization), + Conf.InitThemeAlgorithm + ); + setTourLogo(account.organization.logo); + setOrgIsTourVisible(account.organization.enableTour); + } else { + if (res.data !== "Please login first") { + Setting.showMessage( + "error", + `${i18next.t("application:Failed to sign in")}: ${res.msg}` + ); + } + } - this.setState({ - account: account, - accessToken: accessToken, - }); + this.setState({ + account: account, + accessToken: accessToken, }); + }); } onUpdateAccount(account) { @@ -496,26 +656,45 @@ class App extends Component { footerHtml = footerHtml ?? this.state.application?.footerHtml; return ( - {!this.state.account ? null :
} - {!this.state.account ? null :
} -
+ )} + {!this.state.account ? null : ( +
+ )} +
- { - footerHtml && footerHtml !== "" ? - -
- - : ( - Conf.CustomFooter !== null ? Conf.CustomFooter : ( - - Powered by {"Casdoor"} - - ) - ) - } + }} + > + {footerHtml && footerHtml !== "" ? ( + +
+ + ) : Conf.CustomFooter !== null ? ( + Conf.CustomFooter + ) : ( + + Powered by{" "} + + {"Casdoor"} + + + )}
); @@ -528,15 +707,41 @@ class App extends Component { - help + help AI Assistant - - + + - - + + } @@ -550,19 +755,30 @@ class App extends Component { }} open={this.state.isAiAssistantOpen} > -