diff --git a/.github/workflows/terraform-deploy.yml b/.github/workflows/terraform-deploy.yml new file mode 100644 index 0000000..c0cd34a --- /dev/null +++ b/.github/workflows/terraform-deploy.yml @@ -0,0 +1,112 @@ +name: Terraform CI/CD + +on: + pull_request: + branches: [deploy] + push: + branches: [deploy] + +permissions: + id-token: write + contents: write + pull-requests: write + +env: + TF_VAR_application_name: ${{ vars.APPLICATION_NAME }} + TF_VAR_application_oauth_client_id: ${{ secrets.APPLICATION_OAUTH_CLIENT_ID }} + TF_VAR_application_oauth_client_secret: ${{ secrets.APPLICATION_OAUTH_CLIENT_SECRET }} + TF_VAR_location: ${{ vars.AZURE_LOCATION }} + TF_VAR_execution_mode: ci + ACR_NAME: ${{ vars.ACR_NAME }} + +jobs: + terraform: + runs-on: ubuntu-latest + + steps: + # Checkout repo + - name: Checkout repository + uses: actions/checkout@v4 + + # Install Terraform + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: 1.10.5 + + # Temporarily remove repo provider configuration + - name: Disable repo azurerm provider + working-directory: terraform/azure + run: mv providers.tf providers.tf.bak + + # Create CI-only backend + OIDC provider + - name: Create CI Azure backend/provider + working-directory: terraform/azure + run: | + cat < azure_ci.tf + terraform { + backend "azurerm" {} + } + + provider "azurerm" { + features {} + } + EOF + + - name: Configure Azure OIDC environment + run: | + echo "ARM_USE_OIDC=true" >> $GITHUB_ENV + echo "ARM_CLIENT_ID=${{ secrets.AZURE_CLIENT_ID }}" >> $GITHUB_ENV + echo "ARM_TENANT_ID=${{ secrets.AZURE_TENANT_ID }}" >> $GITHUB_ENV + echo "ARM_SUBSCRIPTION_ID=${{ secrets.AZURE_SUBSCRIPTION_ID }}" >> $GITHUB_ENV + + - name: Azure Login + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Login to ACR + run: | + TOKEN=$(az acr login --name ${{ env.ACR_NAME }} --expose-token --output tsv --query accessToken) + docker login -u 00000000-0000-0000-0000-000000000000 --password-stdin ${{ env.ACR_NAME }}.azurecr.io <<< $TOKEN + + - name: Terraform Init + working-directory: terraform/azure + run: | + terraform init -upgrade -reconfigure\ + -backend-config="resource_group_name=${{ vars.AZURE_RG }}" \ + -backend-config="storage_account_name=${{ vars.AZURE_STORAGE_ACCOUNT }}" \ + -backend-config="container_name=${{ vars.AZURE_CONTAINER }}" \ + -backend-config="key=${{ vars.AZURE_TFSTATE_KEY }}" \ + -backend-config="use_oidc=true" + + - name: Debug backend values + run: | + terraform state pull | head -20 + echo "RG=${{ vars.AZURE_RG }}" + echo "SA=${{ vars.AZURE_STORAGE_ACCOUNT }}" + echo "CONTAINER=${{ vars.AZURE_CONTAINER }}" + echo "KEY=${{ vars.AZURE_TFSTATE_KEY }}" + + # Terraform plan (PR only) + - name: Terraform Plan + if: github.event_name == 'pull_request' + working-directory: terraform/azure + run: terraform plan -out=tfplan + + # Terraform apply (merge to main only) + - name: Terraform Apply + if: github.event_name == 'push' + working-directory: terraform/azure + run: | + terraform plan -out=tfplan + terraform apply -auto-approve tfplan + + # Cleanup CI-only files + - name: Cleanup CI Terraform files + working-directory: terraform/azure + run: | + rm -f azure_ci.tf + mv providers.tf.bak providers.tf \ No newline at end of file diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 2adcb00..d27751c 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -9,6 +9,7 @@ export const authorize = (req: IReqQuery, res: IRes) => { const { query: { redirect_uri: redirectUri, state }, } = req; + return res.render('index.pug', { redirectUri, code: env.AUTHORIZATION_CODE, @@ -20,6 +21,7 @@ export const generateAuthToken = (req: IReq, res: IRes) = const accessToken = jwt.sign({ type: 'access_token', sub: crypto.randomUUID(), email: `${crypto.randomUUID()}@test.com` }, env.JWT_SECRET_KEY, { expiresIn: 3600, }); + const refreshToken = jwt.sign({ type: 'refresh_token' }, env.JWT_SECRET_KEY); const jwtResponse = { @@ -29,9 +31,11 @@ export const generateAuthToken = (req: IReq, res: IRes) = refresh_token: refreshToken, }; - const decodedAuthCode = decodeURIComponent(env.AUTHORIZATION_CODE.replace(/\+/g, '%20')); - - if (req.body.grant_type === 'authorization_code' && req.body.code === decodedAuthCode) { + if ( + req.body.grant_type === 'authorization_code' && + typeof req.body.code === 'string' && + decodeURIComponent(req.body.code.replace(/\+/g, '%20')) === env.AUTHORIZATION_CODE + ) { return res.json(jwtResponse); } else if (req.body.grant_type === 'refresh_token' && req.body.refresh_token) { const payload = jwt.verify(req.body.refresh_token, env.JWT_SECRET_KEY) as JwtPayload; @@ -51,13 +55,17 @@ export const generateAuthToken = (req: IReq, res: IRes) = const decoded = Buffer.from(base64Credentials, 'base64').toString('utf-8'); const [clientId, clientSecret] = decoded.split(':'); - if(clientId === env.OAUTH_CLIENT_ID && clientSecret === env.OAUTH_CLIENT_SECRET){ + const decodedClientId = decodeURIComponent(clientId.replace(/\+/g, '%20')); + const decodedClientSecret = decodeURIComponent(clientSecret.replace(/\+/g, '%20')); + + if(decodedClientId === env.OAUTH_CLIENT_ID && decodedClientSecret === env.OAUTH_CLIENT_SECRET){ return res.json(jwtResponse); } else { throw new Error(); } } - throw new Error(); + + throw new Error(JSON.stringify(req.body)); }; export const getUserInfo = (req: IReq, res: IRes) => { diff --git a/terraform/azure/main.tf b/terraform/azure/main.tf index 84e7e21..7d57bea 100644 --- a/terraform/azure/main.tf +++ b/terraform/azure/main.tf @@ -61,6 +61,7 @@ module "manifest" { "${basename(dirname(var.manifest_files_paths[count.index]))}/${basename(var.manifest_files_paths[count.index])}" ]) + execution_mode = var.execution_mode client_id = local.application_oauth_client_id client_secret = local.application_oauth_client_secret base_url = local.application_service_url diff --git a/terraform/azure/variables.tf b/terraform/azure/variables.tf index e5a21ad..6f9f920 100644 --- a/terraform/azure/variables.tf +++ b/terraform/azure/variables.tf @@ -286,3 +286,9 @@ variable "tags" { description = "A map of the tags to apply to various resources" default = {} } + +variable "execution_mode" { + description = "Execution mode: 'local' or 'ci'" + type = string + default = "local" +} \ No newline at end of file diff --git a/terraform/common/modules/template/main.tf b/terraform/common/modules/template/main.tf index 33228c9..0ab8bcd 100644 --- a/terraform/common/modules/template/main.tf +++ b/terraform/common/modules/template/main.tf @@ -25,11 +25,15 @@ locals { ]... ) ]) - - output_file_path = nonsensitive(local_sensitive_file.this.filename != "/dev/null" ? local_sensitive_file.this.filename : "") } resource "local_sensitive_file" "this" { + count = var.execution_mode == "local" ? 1 : 0 content = local.output_file_content filename = local.enabled ? var.output_file_path : "/dev/null" } + +output "manifest_file_path" { + description = "The intended output file path" + value = var.output_file_path +} diff --git a/terraform/common/modules/template/outputs.tf b/terraform/common/modules/template/outputs.tf index a57d302..07ec913 100644 --- a/terraform/common/modules/template/outputs.tf +++ b/terraform/common/modules/template/outputs.tf @@ -1,4 +1,4 @@ output "output_file_path" { description = "The absolute path to the output file" - value = local.output_file_path + value = var.output_file_path } diff --git a/terraform/common/modules/template/variables.tf b/terraform/common/modules/template/variables.tf index daa27d0..99d4d12 100644 --- a/terraform/common/modules/template/variables.tf +++ b/terraform/common/modules/template/variables.tf @@ -1,3 +1,14 @@ +variable "execution_mode" { + description = "Controls whether Terraform writes manifests to disk (local) or only renders them (ci)" + type = string + default = "local" + + validation { + condition = contains(["local", "ci"], var.execution_mode) + error_message = "execution_mode must be 'local' or 'ci'." + } +} + variable "input_file_path" { description = "The absolute path to the input file. If it doesn't exist, the module will not do anything" type = string