Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/eyqb 319 no public access #182

Merged
merged 33 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3848b74
All controllers redirect to challenge url.
RobertGHippo Jun 5, 2024
0f28df9
Simple cookie-based solution working with hard-coded secret.
RobertGHippo Jun 6, 2024
8e35a35
Postback not quite working yet
RobertGHippo Jun 7, 2024
551496a
Fix assertion
RobertGHippo Jun 7, 2024
b9e6621
Converted raw form to cshtml form helper.
RobertGHippo Jun 10, 2024
c2cd1c2
Allow multiple secret values.
RobertGHippo Jun 10, 2024
450fcd0
Only add the challenge filter if configured.
RobertGHippo Jun 10, 2024
236ce51
Test that only the correct controllers are unguarded.
RobertGHippo Jun 11, 2024
166cca1
Filter passes through if public access allowed.
RobertGHippo Jun 11, 2024
a349039
Don't cache the challenge response.
RobertGHippo Jun 11, 2024
dfa8e17
Logger in the ErrorController is not used.
RobertGHippo Jun 11, 2024
0f9b01a
End-to-end tests now need an auth secret from env.
RobertGHippo Jun 11, 2024
e4a92c0
Tf added gateway exclusion for auth-secret cookie.
RobertGHippo Jun 11, 2024
d9e220d
terraform-docs: automated action
github-actions[bot] Jun 11, 2024
fbe970b
First attempt at hooking up challenge to pipelines, e2e tests etc.
RobertGHippo Jun 12, 2024
b345f78
Merge remote-tracking branch 'origin/feature/eyqb-319-no-public-acces…
RobertGHippo Jun 12, 2024
dc44afe
terraform-docs: automated action
github-actions[bot] Jun 12, 2024
eba3014
Merge branch 'main' into feature/eyqb-319-no-public-access
RobertGHippo Jun 12, 2024
e598888
Make the challenge box a little wider.
RobertGHippo Jun 12, 2024
ccab264
Merge remote-tracking branch 'origin/feature/eyqb-319-no-public-acces…
RobertGHippo Jun 12, 2024
65a54ed
Investigate what happens if I pass secrets in.
RobertGHippo Jun 12, 2024
2735793
Secret parameter passing to run-app-for-testing too.
RobertGHippo Jun 12, 2024
9cb3f95
Fix use of input
RobertGHippo Jun 12, 2024
43867fa
Another attempt at passing the secrets.
RobertGHippo Jun 12, 2024
9196432
Screenshot zips are corrupt in v3: try v4 of the action.
RobertGHippo Jun 12, 2024
0ebd91c
Merge branch 'main' into feature/eyqb-319-no-public-access
sam-c-dfe Jun 13, 2024
58cf097
Set a cookie on the Pa11y tests.
RobertGHippo Jun 13, 2024
6e599b7
Updated to add auth secret to pa11y config
sam-c-dfe Jun 13, 2024
add4048
Added js pa11y-ci implementation
sam-c-dfe Jun 13, 2024
35ae94e
Deleted redundant config file.
RobertGHippo Jun 13, 2024
2556d66
Merge branch 'main' into feature/eyqb-319-no-public-access
RobertGHippo Jun 13, 2024
e225706
Moved tf settings to locals.
RobertGHippo Jun 13, 2024
35fde42
terraform-docs: automated action
github-actions[bot] Jun 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/actions/run-accessibility-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ inputs:
url:
required: true
type: string
auth_secret:
required: true
type: string

runs:
using: composite

steps:
- name: Run Pa11y
shell: bash
run: npm install -g pa11y-ci && pa11y-ci ${{ inputs.url }} --config .pa11yci-ubuntu.json
run: npm install -g pa11y-ci && export AUTH_SECRET=${{ inputs.auth_secret }} && pa11y-ci ${{ inputs.url }} --config .pa11yci-ubuntu.js
working-directory: ./tests/Dfe.EarlyYearsQualification.AccessibilityTests/


11 changes: 10 additions & 1 deletion .github/actions/run-app-for-testing/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ description: Run the app within the action so we can run E2E and Accessibility t

inputs:
url:
description: "URL used by the app to listen"
required: true
type: string
auth_secret:
description: "Auth secret used by end-to-end tests"
required: true
type: string


runs:
using: composite

steps:
- name: Run .Net Project
shell: bash
run: dotnet run --urls "${{ inputs.url }}" --project="src/Dfe.EarlyYearsQualification.Web" --UseMockContentful=true &
run: |
dotnet run --urls "${{ inputs.url }}" --project="src/Dfe.EarlyYearsQualification.Web" \
--UseMockContentful=true \
--ServiceAccess:Keys:0="${{ inputs.auth_secret }}" &
7 changes: 6 additions & 1 deletion .github/actions/run-e2e-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ inputs:
url:
required: true
type: string
auth_secret:
required: true
type: string

runs:
using: composite
Expand All @@ -14,6 +17,7 @@ runs:
uses: cypress-io/github-action@v6
if: success() || failure()
with:
env: auth_secret="${{ inputs.auth_secret }}"
working-directory: ./tests/Dfe.EarlyYearsQualification.E2ETests/
browser: chrome
config: baseUrl="${{ inputs.url }}"
Expand All @@ -22,12 +26,13 @@ runs:
uses: cypress-io/github-action@v6
if: success() || failure()
with:
env: auth_secret="${{ inputs.auth_secret }}"
working-directory: ./tests/Dfe.EarlyYearsQualification.E2ETests/
browser: firefox
config: baseUrl="${{ inputs.url }}"

- name: Store screenshots on test failure
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/code-pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ jobs:
uses: ./.github/actions/run-app-for-testing
with:
url: ${{ env.URL }}
auth_secret: ${{ secrets.WEBAPP_E2E_ACCESS_KEY }}

- name: Run e2e tests
uses: ./.github/actions/run-e2e-tests
with:
url: ${{ env.URL }}
auth_secret: ${{ secrets.WEBAPP_E2E_ACCESS_KEY }}

- name: Run Accessibility tests
uses: ./.github/actions/run-accessibility-tests
with:
url: ${{ env.URL }}
auth_secret: ${{ secrets.WEBAPP_E2E_ACCESS_KEY }}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Dfe.EarlyYearsQualification.Content.Entities;
using Dfe.EarlyYearsQualification.Content.Renderers.Entities;
using Dfe.EarlyYearsQualification.Content.Services;
using Dfe.EarlyYearsQualification.Web.Controllers.Base;
using Dfe.EarlyYearsQualification.Web.Models.Content;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -11,7 +12,7 @@ public class AccessibilityStatementController(
ILogger<AccessibilityStatementController> logger,
IContentService contentService,
IHtmlRenderer renderer)
: Controller
: ServiceController
{
[HttpGet]
public async Task<IActionResult> Index()
Expand All @@ -21,7 +22,7 @@ public async Task<IActionResult> Index()
if (content is null)
{
logger.LogError("No content for the accessibility statement page");
return RedirectToAction("Error", "Home");
return RedirectToAction("Index", "Error");
}

var model = await Map(content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
using Dfe.EarlyYearsQualification.Content.Entities;
using Dfe.EarlyYearsQualification.Content.Renderers.Entities;
using Dfe.EarlyYearsQualification.Content.Services;
using Dfe.EarlyYearsQualification.Web.Controllers.Base;
using Dfe.EarlyYearsQualification.Web.Models.Content;
using Microsoft.AspNetCore.Mvc;

namespace Dfe.EarlyYearsQualification.Web.Controllers;

[Route("/advice")]
public class AdviceController(ILogger<AdviceController> logger, IContentService contentService, IHtmlRenderer renderer)
: Controller
: ServiceController
{
[HttpGet("qualification-outside-the-united-kingdom")]
public async Task<IActionResult> QualificationOutsideTheUnitedKingdom()
Expand All @@ -22,8 +23,8 @@ private async Task<IActionResult> GetView(string advicePageId)
var advicePage = await contentService.GetAdvicePage(advicePageId);
if (advicePage is null)
{
logger.LogError("No content for the advice page");
return RedirectToAction("Error", "Home");
logger.LogError("No content for the advice page");
return RedirectToAction("Index", "Error");
}

var model = await Map(advicePage);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Dfe.EarlyYearsQualification.Web.Filters;
using Microsoft.AspNetCore.Mvc;

namespace Dfe.EarlyYearsQualification.Web.Controllers.Base;

/// <summary>
/// Controller class that is guarded by a <see cref="IChallengeResourceFilterAttribute" />
/// so that it is possible, while in private beta and in non-production environments,
/// to configure a secret that must be entered to gain access to the service.
/// All controllers except <see cref="ChallengeController" />, <see cref="ErrorController" />
/// and <see cref="HealthController" /> should derive from this type.
/// </summary>
[ServiceFilter<IChallengeResourceFilterAttribute>]
public class ServiceController : Controller;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Dfe.EarlyYearsQualification.Web.Filters;
using Dfe.EarlyYearsQualification.Web.Models;
using Microsoft.AspNetCore.Mvc;

namespace Dfe.EarlyYearsQualification.Web.Controllers;

public class ChallengeController(
ILogger<ChallengeController> logger,
IUrlHelper urlHelper)
: Controller
{
private const string DefaultRedirectAddress = "/";

[HttpGet]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public Task<IActionResult> Index([FromQuery] ChallengeModel model)
{
if (!ModelState.IsValid)
{
logger.LogWarning("Invalid challenge model (get)");
}

model.RedirectAddress = SanitiseReferralAddress(model.RedirectAddress);

logger.LogWarning("Challenge page invoked");

return Task.FromResult<IActionResult>(View("EntryForm", model));
}

[HttpPost]
public Task<IActionResult> Post([FromForm] ChallengeModel model)
{
if (!ModelState.IsValid)
{
logger.LogWarning("Invalid challenge model (post)");
}

var referralAddress = SanitiseReferralAddress(model.RedirectAddress);

if (model.Value != null)
{
logger.LogInformation("Challenge secret access value entered successfully");

SetAuthSecretCookie(model.Value);
return Task.FromResult<IActionResult>(new RedirectResult(referralAddress));
}

return Index(model);
}

private void SetAuthSecretCookie(string accessValue)
{
HttpContext.Response
.Cookies
.Append(ChallengeResourceFilterAttribute.AuthSecretCookieName, accessValue);
}

private string SanitiseReferralAddress(string? from)
{
var redirectAddress = from ?? DefaultRedirectAddress;

if (!urlHelper.IsLocalUrl(redirectAddress))
{
redirectAddress = DefaultRedirectAddress;
}

return redirectAddress;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Dfe.EarlyYearsQualification.Content.Entities;
using Dfe.EarlyYearsQualification.Content.Renderers.Entities;
using Dfe.EarlyYearsQualification.Content.Services;
using Dfe.EarlyYearsQualification.Web.Controllers.Base;
using Dfe.EarlyYearsQualification.Web.Models.Content;
using Dfe.EarlyYearsQualification.Web.Services.CookieService;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -15,7 +16,7 @@ public class CookiesController(
ISuccessBannerRenderer successBannerRenderer,
ICookieService cookieService,
IUrlHelper urlHelper)
: Controller
: ServiceController
{
[HttpGet]
public async Task<IActionResult> Index()
Expand All @@ -25,7 +26,7 @@ public async Task<IActionResult> Index()
if (content is null)
{
logger.LogError("No content for the cookies page");
return RedirectToAction("Error", "Home");
return RedirectToAction("Index", "Error");
}

var model = await Map(content);
Expand All @@ -34,21 +35,21 @@ public async Task<IActionResult> Index()
}

[HttpPost("accept")]
public IActionResult Accept([FromForm]string? returnUrl)
public IActionResult Accept([FromForm] string? returnUrl)
{
cookieService.SetPreference(true);
return Redirect(CheckUrl(returnUrl));
}

[HttpPost("reject")]
public IActionResult Reject([FromForm]string? returnUrl)
public IActionResult Reject([FromForm] string? returnUrl)
{
cookieService.RejectCookies();
return Redirect(CheckUrl(returnUrl));
}

[HttpPost("hidebanner")]
public IActionResult HideBanner([FromForm]string? returnUrl)
public IActionResult HideBanner([FromForm] string? returnUrl)
{
cookieService.SetVisibility(false);
return Redirect(CheckUrl(returnUrl));
Expand All @@ -65,13 +66,14 @@ public IActionResult CookiePreference(string cookiesAnswer)
{
cookieService.RejectCookies();
}

TempData["UserPreferenceRecorded"] = true;
return Redirect("/cookies");
}

private string CheckUrl(string? url)
{
return urlHelper.IsLocalUrl(url) ? url : "/cookies";
return urlHelper.IsLocalUrl(url) ? url : "/cookies";
}

private async Task<CookiesPageModel> Map(CookiesPage content)
Expand Down
16 changes: 16 additions & 0 deletions src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Diagnostics;
using Dfe.EarlyYearsQualification.Web.Models;
using Microsoft.AspNetCore.Mvc;

namespace Dfe.EarlyYearsQualification.Web.Controllers;

[Route("/error")]
public class ErrorController : Controller
{
[HttpGet]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Index()
{
return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
13 changes: 3 additions & 10 deletions src/Dfe.EarlyYearsQualification.Web/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using System.Diagnostics;
using Dfe.EarlyYearsQualification.Content.Entities;
using Dfe.EarlyYearsQualification.Content.Renderers.Entities;
using Dfe.EarlyYearsQualification.Content.Services;
using Dfe.EarlyYearsQualification.Web.Models;
using Dfe.EarlyYearsQualification.Web.Controllers.Base;
using Dfe.EarlyYearsQualification.Web.Models.Content;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -13,7 +12,7 @@ public class HomeController(
IContentService contentService,
IHtmlRenderer htmlRenderer,
ISideContentRenderer sideContentRenderer)
: Controller
: ServiceController
{
[HttpGet]
public async Task<IActionResult> Index()
Expand All @@ -22,19 +21,13 @@ public async Task<IActionResult> Index()
if (startPageContent is null)
{
logger.LogCritical("Start page content not found");
return RedirectToAction("Error");
return RedirectToAction("Index", "Error");
}

var model = await Map(startPageContent);
return View(model);
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

private async Task<StartPageModel> Map(StartPage startPageContent)
{
return new StartPageModel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Dfe.EarlyYearsQualification.Content.Entities;
using Dfe.EarlyYearsQualification.Content.Renderers.Entities;
using Dfe.EarlyYearsQualification.Content.Services;
using Dfe.EarlyYearsQualification.Web.Controllers.Base;
using Dfe.EarlyYearsQualification.Web.Models.Content;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -12,7 +13,7 @@ public class QualificationDetailsController(
ILogger<QualificationDetailsController> logger,
IContentService contentService,
IGovUkInsetTextRenderer renderer)
: Controller
: ServiceController
{
[HttpGet]
public IActionResult Get()
Expand All @@ -32,7 +33,7 @@ public async Task<IActionResult> Index(string qualificationId)
if (detailsPageContent is null)
{
logger.LogError("No content for the qualification details page");
return RedirectToAction("Error", "Home");
return RedirectToAction("Index", "Error");
}

var qualification = await contentService.GetQualificationById(qualificationId);
Expand All @@ -42,7 +43,7 @@ public async Task<IActionResult> Index(string qualificationId)
logger.LogError("Could not find details for qualification with ID: {QualificationId}",
loggedQualificationId);

return RedirectToAction("Error", "Home");
return RedirectToAction("Index", "Error");
}

var model = await Map(qualification, detailsPageContent);
Expand Down
Loading