diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml new file mode 100644 index 0000000..1765622 --- /dev/null +++ b/.github/workflows/pull-request.yaml @@ -0,0 +1,46 @@ +name: Checks + +on: + workflow_dispatch: {} + pull_request: {} + +# When a new revision is pushed to a PR, cancel all in-progress CI runs for that +# PR. See https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - uses: actions/checkout@v4 + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "6.0.x" + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build + + smoke-test: + name: Smoke Test + runs-on: ubuntu-latest + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - uses: actions/checkout@v4 + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "6.0.x" + - name: Install dependencies + working-directory: test + run: dotnet restore + - name: Test + working-directory: test + run: dotnet test diff --git a/Api/Api.csproj b/Api/Api.csproj index 0341a93..91a1f87 100644 --- a/Api/Api.csproj +++ b/Api/Api.csproj @@ -10,12 +10,12 @@ true 1591 https://github.com/StyraInc/opa-csharp + true - diff --git a/test/SmokeTest.Tests/OPAEvalRBACTest.cs b/test/SmokeTest.Tests/OPAEvalRBACTest.cs new file mode 100644 index 0000000..7cb0bbe --- /dev/null +++ b/test/SmokeTest.Tests/OPAEvalRBACTest.cs @@ -0,0 +1,67 @@ +using Api; +using Api.Models.Requests; +using Api.Models.Components; + +namespace SmokeTest.Tests; + +public class OPAEvalRBACTest +{ + [Fact] + public async Task HelloTestContainers() + { + // Read in the test data files. + var policy = System.IO.File.ReadAllBytes(Path.Combine("testdata", "policy.rego")); + var data = System.IO.File.ReadAllBytes(Path.Combine("testdata", "data.json")); + + // Create a new instance of a container. + var container = new ContainerBuilder() + .WithImage("openpolicyagent/opa:latest") + // Bind port 8181 of the container to a random port on the host. + .WithPortBinding(8181, true) + .WithCommand("run", "--server", "policy.rego", "data.json") + // Map our policy and data files into the container instance. + .WithResourceMapping(policy, "policy.rego") + .WithResourceMapping(data, "data.json") + // Wait until the HTTP endpoint of the container is available. + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(8181).ForPath("/health"))) + // Build the container configuration. + .Build(); + + // Start the container. + await container.StartAsync() + .ConfigureAwait(false); + + // Create a new instance of HttpClient to send HTTP requests. + var httpClient = new HttpClient(); + + // Construct the request URI by specifying the scheme, hostname, assigned random host port, and the endpoint "uuid". + var requestUri = new UriBuilder(Uri.UriSchemeHttp, container.Hostname, container.GetMappedPublicPort(8181)).Uri; + + // Send an HTTP GET request to the specified URI and retrieve the response as a string. + var sdk = new Opa(serverIndex: 0, serverUrl: requestUri.ToString()); + + // Exercise the low-level OPA C# SDK. + ExecutePolicyWithInputRequest req = new ExecutePolicyWithInputRequest() + { + Path = "app/rbac", + RequestBody = new ExecutePolicyWithInputRequestBody() + { + Input = Input.CreateMapOfany( + new Dictionary() { + { "user", "alice" }, + { "action", "read" }, + { "object", "id123" }, + { "type", "dog" }, + }), + }, + }; + + var res = Task.Run(() => sdk.ExecutePolicyWithInputAsync(req)).GetAwaiter().GetResult(); + var resultMap = res.SuccessfulPolicyEvaluation?.Result?.MapOfany; + + // Ensure we got back the expected fields from the eval. + Assert.Equal(true, resultMap?.GetValueOrDefault("allow", false)); + Assert.Equal(true, resultMap?.GetValueOrDefault("user_is_admin", false)); + Assert.Equal(new List(), resultMap?.GetValueOrDefault("user_is_granted")); + } +} \ No newline at end of file diff --git a/test/SmokeTest.Tests/SmokeTest.Tests.csproj b/test/SmokeTest.Tests/SmokeTest.Tests.csproj new file mode 100644 index 0000000..a8238cc --- /dev/null +++ b/test/SmokeTest.Tests/SmokeTest.Tests.csproj @@ -0,0 +1,39 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + diff --git a/test/SmokeTest.Tests/Usings.cs b/test/SmokeTest.Tests/Usings.cs new file mode 100644 index 0000000..6c9d8e7 --- /dev/null +++ b/test/SmokeTest.Tests/Usings.cs @@ -0,0 +1,6 @@ +global using Xunit; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Containers; +global using DotNet.Testcontainers.Images; +global using DotNet.Testcontainers.Networks; +global using Newtonsoft.Json; \ No newline at end of file diff --git a/test/SmokeTest.Tests/testdata/data.json b/test/SmokeTest.Tests/testdata/data.json new file mode 100644 index 0000000..88ac41b --- /dev/null +++ b/test/SmokeTest.Tests/testdata/data.json @@ -0,0 +1,63 @@ +{ + "user_roles": { + "alice": [ + "admin" + ], + "bob": [ + "employee", + "billing" + ], + "eve": [ + "customer" + ] + }, + "role_grants": { + "customer": [ + { + "action": "read", + "type": "dog" + }, + { + "action": "read", + "type": "cat" + }, + { + "action": "adopt", + "type": "dog" + }, + { + "action": "adopt", + "type": "cat" + } + ], + "employee": [ + { + "action": "read", + "type": "dog" + }, + { + "action": "read", + "type": "cat" + }, + { + "action": "update", + "type": "dog" + }, + { + "action": "update", + "type": "cat" + } + ], + "billing": [ + { + "action": "read", + "type": "finance" + }, + { + "action": "update", + "type": "finance" + } + ] + } +} + diff --git a/test/SmokeTest.Tests/testdata/policy.rego b/test/SmokeTest.Tests/testdata/policy.rego new file mode 100644 index 0000000..35686b0 --- /dev/null +++ b/test/SmokeTest.Tests/testdata/policy.rego @@ -0,0 +1,52 @@ +# Role-based Access Control (RBAC) +# -------------------------------- +# +# This example defines an RBAC model for a Pet Store API. The Pet Store API allows +# users to look at pets, adopt them, update their stats, and so on. The policy +# controls which users can perform actions on which resources. The policy implements +# a classic Role-based Access Control model where users are assigned to roles and +# roles are granted the ability to perform some action(s) on some type of resource. +# +# This example shows how to: +# +# * Define an RBAC model in Rego that interprets role mappings represented in JSON. +# * Iterate/search across JSON data structures (e.g., role mappings) +# +# For more information see: +# +# * Rego comparison to other systems: https://www.openpolicyagent.org/docs/latest/comparison-to-other-systems/ +# * Rego Iteration: https://www.openpolicyagent.org/docs/latest/#iteration + +package app.rbac + +import rego.v1 + +# By default, deny requests. +default allow := false + +# Allow admins to do anything. +allow if user_is_admin + +# Allow the action if the user is granted permission to perform the action. +allow if { + # Find grants for the user. + some grant in user_is_granted + + # Check if the grant permits the action. + input.action == grant.action + input.type == grant.type +} + +# user_is_admin is true if "admin" is among the user's roles as per data.user_roles +user_is_admin if "admin" in data.user_roles[input.user] + +# user_is_granted is a set of grants for the user identified in the request. +# The `grant` will be contained if the set `user_is_granted` for every... +user_is_granted contains grant if { + # `role` assigned an element of the user_roles for this user... + some role in data.user_roles[input.user] + + # `grant` assigned a single grant from the grants list for 'role'... + some grant in data.role_grants[role] +} + diff --git a/test/test.sln b/test/test.sln new file mode 100644 index 0000000..d6630b6 --- /dev/null +++ b/test/test.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmokeTest.Tests", "SmokeTest.Tests\SmokeTest.Tests.csproj", "{32BE0D80-A8E6-49A3-822B-C73768A1423D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {32BE0D80-A8E6-49A3-822B-C73768A1423D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32BE0D80-A8E6-49A3-822B-C73768A1423D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32BE0D80-A8E6-49A3-822B-C73768A1423D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32BE0D80-A8E6-49A3-822B-C73768A1423D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal