From 388630984cea12ba0adf9bf708c9abd2add3ce7d Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Tue, 19 Mar 2024 12:29:03 +0000 Subject: [PATCH 1/8] Authorize on proposal and session membership --- .devcontainer/docker-compose.yml | 2 +- policy/system.rego | 22 +++++++++++++++++++++- policy/token.rego | 26 ++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 policy/token.rego diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index d237c02..937c979 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -27,7 +27,7 @@ services: - ../policy/:/policy:cached,z env_file: opa.env environment: - JWKS_ENDPOINT: https://authn.diamond.ac.uk/realms/master/protocol/openid-connect/certs + SKIP_AUTHORIZATION: "true" ispyb: image: ghcr.io/diamondlightsource/ispyb-database:v3.0.0 diff --git a/policy/system.rego b/policy/system.rego index 0b9d475..debd5db 100644 --- a/policy/system.rego +++ b/policy/system.rego @@ -1,11 +1,31 @@ package system +import data.token import rego.v1 +# METADATA +# description: Allow subjects on session or containing proposal +# entrypoint: true main := {"allow": allow} default allow := false +# Allow if the SKIP_AUTHORIZATION environment variable is set and a preset token is supplied allow if { - input.token == "ValidToken" + opa.runtime().env.SKIP_AUTHORIZATION + input.token == "ValidToken" +} + +# Allow if on proposal which contains session +allow if { + some proposal_number in data.diamond.data.subjects[token.claims.fedid].proposals + proposal_number == input.proposal +} + +# Allow if directly on session +allow if { + some session_id in data.diamond.data.subjects[token.claims.fedid].sessions + session := data.diamond.data.sessions[session_id] + session.proposal_number == input.proposal + session.visit_number == input.visit } diff --git a/policy/token.rego b/policy/token.rego new file mode 100644 index 0000000..f9e4520 --- /dev/null +++ b/policy/token.rego @@ -0,0 +1,26 @@ +package token + +import rego.v1 + +fetch_jwks(url) := http.send({ + "url": jwks_url, + "method": "GET", + "force_cache": true, + "force_cache_duration_seconds": 3600, +}) + +jwks_endpoint := opa.runtime().env.JWKS_ENDPOINT + +unverified := io.jwt.decode(input.token) + +jwt_header := unverified[0] + +jwks_url := concat("?", [jwks_endpoint, urlquery.encode_object({"kid": jwt_header.kid})]) + +jwks := fetch_jwks(jwks_url).raw_body + +verified := unverified if { + io.jwt.verify_rs256(input.token, jwks) +} + +claims := verified[1] From a3c5c639764b8c49a560ab6c094356782ba0c99a Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Tue, 19 Mar 2024 13:07:34 +0000 Subject: [PATCH 2/8] Lint, test & containerise policy in CI --- .github/workflows/policy.yml | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/workflows/policy.yml diff --git a/.github/workflows/policy.yml b/.github/workflows/policy.yml new file mode 100644 index 0000000..d582ca3 --- /dev/null +++ b/.github/workflows/policy.yml @@ -0,0 +1,75 @@ +name: Policy + +on: + push: + pull_request: + +jobs: + lint: + # Deduplicate jobs from pull requests and branch pushes within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v4.1.2 + + - name: Setup Regal + uses: StyraInc/setup-regal@v1.0.0 + with: + version: latest + + - name: Lint + run: regal lint --format github ./policy + + test: + # Deduplicate jobs from pull requests and branch pushes within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v4.1.2 + + - name: Setup OPA + uses: open-policy-agent/setup-opa@v2.2.0 + with: + version: latest + + - name: Test + run: opa test ./policy -v + + build_bundle: + needs: + - lint + - test + # Deduplicate jobs from pull requests and branch pushes within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout source + uses: actions/checkout@v4.1.2 + + - name: Generate Image Name + run: echo IMAGE_REPOSITORY=ghcr.io/$(echo "${{ github.repository }}-policy" | tr '[:upper:]' '[:lower:]' | tr '[_]' '[\-]') >> $GITHUB_ENV + + - name: Log in to GitHub Docker Registry + uses: docker/login-action@v3.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup OPA + uses: open-policy-agent/setup-opa@v2.2.0 + with: + version: latest + + - name: Build OPA Policy # If this is a tag, use it as a revision string + run: opa build -b policy -r ${{ github.ref_name }} --ignore *_test.rego + + - name: Publish OPA Bundle + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} + run: oras push ${{ env.IMAGE_REPOSITORY }}:${{ github.ref_name }} bundle.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip + From 1eeaf45841255234866ab2796e0719b87ae944b5 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Tue, 19 Mar 2024 14:17:23 +0000 Subject: [PATCH 3/8] Structure proposal information in graphql --- models/build.rs | 2 +- sessions/src/graphql.rs | 74 +++++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/models/build.rs b/models/build.rs index 5c6c62f..9f3fb5c 100644 --- a/models/build.rs +++ b/models/build.rs @@ -28,7 +28,7 @@ const TABLES_SPECS: &[&Table] = &[ }, &Table { name: "Proposal", - columns: &["proposalId", "proposalNumber"], + columns: &["proposalId", "proposalCode", "proposalNumber"], }, ]; diff --git a/sessions/src/graphql.rs b/sessions/src/graphql.rs index 318cec8..183ad2e 100644 --- a/sessions/src/graphql.rs +++ b/sessions/src/graphql.rs @@ -1,13 +1,13 @@ use crate::opa::{OpaClient, OpaInput}; use async_graphql::{ - Context, EmptyMutation, EmptySubscription, Object, Schema, SchemaBuilder, SimpleObject, + ComplexObject, Context, EmptyMutation, EmptySubscription, Object, Schema, SchemaBuilder, + SimpleObject, }; use chrono::{DateTime, Utc}; use models::{bl_session, proposal}; -use sea_orm::{ - ColumnTrait, Condition, DatabaseConnection, EntityTrait, JoinType, QueryFilter, QuerySelect, -}; +use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter}; use serde::Serialize; +use tracing::instrument; /// The GraphQL schema exposed by the service pub type RootSchema = Schema; @@ -19,22 +19,48 @@ pub fn root_schema_builder() -> SchemaBuilder, - /// The date and time at which the Session began - start: Option>, - /// The date and time at which the Session ended - end: Option>, + /// The underlying database model + #[graphql(skip)] + session: bl_session::Model, + /// The proposal information + proposal: Option, } -impl From for Session { - fn from(value: bl_session::Model) -> Self { - Self { - visit_number: value.visit_number, - start: value.start_date.map(|date| date.and_utc()), - end: value.end_date.map(|date| date.and_utc()), - } +#[ComplexObject] +impl Session { + async fn visit_number(&self, _ctx: &Context<'_>) -> u32 { + self.session.visit_number.unwrap_or_default() + } + + async fn start(&self, _ctx: &Context<'_>) -> Option> { + self.session.start_date.map(|date| date.and_utc()) + } + + async fn end(&self, _ctx: &Context<'_>) -> Option> { + self.session.end_date.map(|date| date.and_utc()) + } +} + +/// An Experimental Proposal, containing numerous sessions +#[derive(Debug)] +struct Proposal(proposal::Model); + +#[Object] +impl Proposal { + async fn code(&self, _ctx: &Context<'_>) -> &Option { + &self.0.proposal_code + } + + /// A unique number identifying the Proposal + async fn number(&self, _ctx: &Context<'_>) -> Result, async_graphql::Error> { + Ok(self + .0 + .proposal_number + .as_ref() + .map(|num| num.parse()) + .transpose()?) } } @@ -54,6 +80,7 @@ struct OpaSessionParameters { #[Object] impl RootQuery { /// Retrieves a Beamline Session + #[instrument(name = "query_session", skip(ctx))] async fn session( &self, ctx: &Context<'_>, @@ -68,13 +95,7 @@ impl RootQuery { )?) .await?; Ok(bl_session::Entity::find() - .join_rev( - JoinType::InnerJoin, - proposal::Entity::has_many(bl_session::Entity) - .from(proposal::Column::ProposalId) - .to(bl_session::Column::ProposalId) - .into(), - ) + .find_also_related(proposal::Entity) .filter( Condition::all() .add(bl_session::Column::VisitNumber.eq(visit)) @@ -82,6 +103,9 @@ impl RootQuery { ) .one(database) .await? - .map(Session::from)) + .map(|(session, proposal)| Session { + session, + proposal: proposal.map(Proposal), + })) } } From 6f2c8c104191c7660900ac8017156af9c45afb78 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Tue, 19 Mar 2024 14:30:37 +0000 Subject: [PATCH 4/8] Enable GraphQL federation --- sessions/src/graphql.rs | 13 ++++++++++++- sessions/src/main.rs | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/sessions/src/graphql.rs b/sessions/src/graphql.rs index 183ad2e..108ea91 100644 --- a/sessions/src/graphql.rs +++ b/sessions/src/graphql.rs @@ -14,7 +14,7 @@ pub type RootSchema = Schema; /// A schema builder for the service pub fn root_schema_builder() -> SchemaBuilder { - Schema::build(RootQuery, EmptyMutation, EmptySubscription) + Schema::build(RootQuery, EmptyMutation, EmptySubscription).enable_federation() } /// A Beamline Session @@ -108,4 +108,15 @@ impl RootQuery { proposal: proposal.map(Proposal), })) } + + /// Retrieves a Beamline Session + #[graphql(entity)] + async fn router_session( + &self, + ctx: &Context<'_>, + proposal: u32, + visit: u32, + ) -> Result, async_graphql::Error> { + self.session(ctx, proposal, visit).await + } } diff --git a/sessions/src/main.rs b/sessions/src/main.rs index 12fae42..4a234c6 100644 --- a/sessions/src/main.rs +++ b/sessions/src/main.rs @@ -17,7 +17,7 @@ use crate::{ opa::OpaClient, route_handlers::GraphQLHandler, }; -use async_graphql::{extensions::Tracing, http::GraphiQLSource}; +use async_graphql::{extensions::Tracing, http::GraphiQLSource, SDLExportOptions}; use axum::{response::Html, routing::get, Router}; use clap::Parser; use opentelemetry_otlp::WithExportConfig; @@ -93,7 +93,7 @@ async fn main() { } Cli::Schema(args) => { let schema = root_schema_builder().finish(); - let schema_string = schema.sdl(); + let schema_string = schema.sdl_with_options(SDLExportOptions::new().federation()); if let Some(path) = args.path { let mut file = File::create(path).unwrap(); file.write_all(schema_string.as_bytes()).unwrap(); From 28c169ad9221b269423645fca5b4b70d07bc9028 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Tue, 19 Mar 2024 14:53:49 +0000 Subject: [PATCH 5/8] Group dependabot updates --- .github/dependabot.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1556031..590cba4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,13 +4,27 @@ updates: directory: / schedule: interval: weekly + groups: + minor: + update-types: + - minor + - patch - package-ecosystem: devcontainers - directory: "/" + directory: / schedule: interval: weekly + groups: + minor: + update-types: + - minor + - patch - package-ecosystem: cargo directory: / schedule: interval: weekly + groups: + patch: + update-types: + - patch From c5d936000f202d4f8dd6b649bf251ab196a08c72 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Tue, 19 Mar 2024 14:54:02 +0000 Subject: [PATCH 6/8] Add docker dependencies to dependabot config --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 590cba4..8b54aee 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,6 +20,16 @@ updates: - minor - patch + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + groups: + minor: + update-types: + - minor + - patch + - package-ecosystem: cargo directory: / schedule: From f9f337940be1581e366b6cff6213d686b672ce63 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Wed, 20 Mar 2024 09:52:40 +0000 Subject: [PATCH 7/8] Publish schema to apollo studio --- .github/dependabot.yml | 3 ++ .github/workflows/schema.yml | 83 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 .github/workflows/schema.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8b54aee..10a7531 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,9 @@ updates: schedule: interval: weekly groups: + github-artifacts: + patterns: + - actions/*-artifact minor: update-types: - minor diff --git a/.github/workflows/schema.yml b/.github/workflows/schema.yml new file mode 100644 index 0000000..ead6cb6 --- /dev/null +++ b/.github/workflows/schema.yml @@ -0,0 +1,83 @@ +name: Schema + +on: + push: + pull_request: + +jobs: + generate: + # Deduplicate jobs from pull requests and branch pushes within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + services: + ispyb: + image: ghcr.io/diamondlightsource/ispyb-database:v3.0.0 + ports: + - 3306:3306 + env: + MARIADB_ROOT_PASSWORD: rootpassword + options: > + --health-cmd "/usr/local/bin/healthcheck.sh --defaults-file=/ispyb/.my.cnf --connect" + env: + DATABASE_URL: mysql://root:rootpassword@localhost:3306/ispyb_build + steps: + - name: Checkout source + uses: actions/checkout@v4.1.2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + default: true + + - name: Cache Rust Build + uses: Swatinem/rust-cache@v2.7.3 + + - name: Generate Schema + uses: actions-rs/cargo@v1.0.3 + with: + command: run + args: > + schema + --path sessions.graphql + + - name: Upload Schema Artifact + uses: actions/upload-artifact@v4.3.1 + with: + name: sessions.graphql + path: sessions.graphql + + publish: + # Deduplicate jobs from pull requests and branch pushes within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + needs: + - generate + runs-on: ubuntu-latest + steps: + - name: Install Rover CLI + run: | + curl -sSL https://rover.apollo.dev/nix/v0.23.0-rc.3 | sh + echo "$HOME/.rover/bin" >> $GITHUB_PATH + + - name: Download Schema Artifact + uses: actions/download-artifact@v4.1.4 + with: + name: sessions.graphql + + - name: Check Subgraph Schema + run: > + rover subgraph check data-gateway-n63jcf@current + --schema sessions.graphql + --name sessions + env: + APOLLO_KEY: ${{ secrets.APOLLO_STUDIO }} + + - name: Publish Subgraph Schema to Apollo Studio + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} + run: > + rover subgraph publish data-gateway-n63jcf@current + --routing-url http://sessions:80 + --schema sessions.graphql + --name sessions + env: + APOLLO_KEY: ${{ secrets.APOLLO_STUDIO }} From 01339faa686409eb5f2c9d228fd9f369ab4ca6b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:54:02 +0000 Subject: [PATCH 8/8] Bump the minor group with 5 updates Bumps the minor group with 5 updates: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `4.1.1` | `4.1.2` | | [docker/login-action](https://github.com/docker/login-action) | `3.0.0` | `3.1.0` | | [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3.1.0` | `3.2.0` | | [docker/build-push-action](https://github.com/docker/build-push-action) | `5.2.0` | `5.3.0` | | [devcontainers/ci](https://github.com/devcontainers/ci) | `0.3.1900000347` | `0.3.1900000348` | Updates `actions/checkout` from 4.1.1 to 4.1.2 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.1...v4.1.2) Updates `docker/login-action` from 3.0.0 to 3.1.0 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v3...v3.1.0) Updates `docker/setup-buildx-action` from 3.1.0 to 3.2.0 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3.1.0...v3.2.0) Updates `docker/build-push-action` from 5.2.0 to 5.3.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v5.2.0...v5.3.0) Updates `devcontainers/ci` from 0.3.1900000347 to 0.3.1900000348 - [Release notes](https://github.com/devcontainers/ci/releases) - [Commits](https://github.com/devcontainers/ci/compare/v0.3.1900000347...v0.3.1900000348) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor - dependency-name: devcontainers/ci dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor ... Signed-off-by: dependabot[bot] --- .github/workflows/code.yml | 4 ++-- .github/workflows/container.yml | 8 ++++---- .github/workflows/devcontainer.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index b6edbd5..e35ae89 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -22,7 +22,7 @@ jobs: DATABASE_URL: mysql://root:rootpassword@localhost/ispyb_build steps: - name: Checkout source - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Install dependencies uses: awalsh128/cache-apt-pkgs-action@v1.4.2 @@ -79,7 +79,7 @@ jobs: DATABASE_URL: mysql://root:rootpassword@localhost/ispyb_build steps: - name: Checkout source - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Install dependencies uses: awalsh128/cache-apt-pkgs-action@v1.4.2 diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 44a4196..3730815 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -23,14 +23,14 @@ jobs: packages: write steps: - name: Checkout Code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Generate Image Name run: echo IMAGE_REPOSITORY=ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]' | tr '[_]' '[\-]') >> $GITHUB_ENV - name: Log in to GitHub Docker Registry if: github.event_name != 'pull_request' - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.1.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -46,12 +46,12 @@ jobs: type=raw,value=latest - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.1.0 + uses: docker/setup-buildx-action@v3.2.0 with: driver-opts: network=host - name: Build Image - uses: docker/build-push-action@v5.2.0 + uses: docker/build-push-action@v5.3.0 with: build-args: DATABASE_URL=mysql://root:rootpassword@localhost:3306/ispyb_build target: deploy diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml index f409bcb..73e6d47 100644 --- a/.github/workflows/devcontainer.yml +++ b/.github/workflows/devcontainer.yml @@ -11,13 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.1.0 + uses: docker/setup-buildx-action@v3.2.0 - name: Create .env file run: touch .devcontainer/opa.env - name: Build dev container - uses: devcontainers/ci@v0.3.1900000347 + uses: devcontainers/ci@v0.3.1900000348