Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ AWS_REGION=us-east-2

ENVELOPE_DOWNLOADS_BUCKET=envelope-downloads

ENVELOPE_GRAPHS_BUCKET=

POSTGRESQL_ADDRESS=localhost
POSTGRESQL_USERNAME=metadataregistry
POSTGRESQL_PASSWORD=metadataregistry
Expand Down
47 changes: 12 additions & 35 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

name: Run linter and tests

on:
Expand Down Expand Up @@ -41,6 +42,8 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive


- name: Pre-cache grape-middleware-logger gem
run: |
Expand All @@ -51,7 +54,13 @@ jobs:

- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
bundler-cache: false
ruby-version: '3.4'
- name: Install gems (non-frozen)
run: |
bundle config set path vendor/bundle
bundle config set frozen false
bundle install --jobs 4
- run: RACK_ENV=test bundle exec rake db:migrate
# Rubocop, bundler-audit, etc. are executed through Overcommit hooks.

Expand All @@ -70,40 +79,8 @@ jobs:
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}

- name: Upload coverage report
if: always()
if: ${{ always() && hashFiles('coverage/**') != '' }}
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage

semgrep:
name: "Semgrep SAST"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install Semgrep
run: |
python3 -m pip install --upgrade pip
python3 -m pip install semgrep
- name: Run Semgrep (Ruby/JS)
run: |
semgrep --config p/r2c-security-audit \
--include app --include lib \
--error --timeout 180
- name: Export Semgrep SARIF
if: always()
run: |
semgrep --config p/r2c-security-audit \
--include app --include lib \
--sarif -o semgrep.sarif || true
- name: Upload Semgrep SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
path: coverage/
1 change: 1 addition & 0 deletions app/api/v1/publish.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'policies/envelope_policy'
require 'services/publish_interactor'
require 'services/sync_envelope_graph_with_s3'

module API
module V1
Expand Down
10 changes: 10 additions & 0 deletions app/models/envelope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ class Envelope < ActiveRecord::Base
before_validation :process_resource, :process_headers
before_save :assign_last_verified_on
after_save :update_headers
after_save :upload_to_s3
before_destroy :delete_description_sets, prepend: true
after_destroy :delete_from_ocn
after_destroy :delete_from_s3
after_commit :export_to_ocn

validates :envelope_community, :envelope_type, :envelope_version,
Expand Down Expand Up @@ -260,4 +262,12 @@ def export_to_ocn

ExportToOCNJob.perform_later(id)
end

def upload_to_s3
SyncEnvelopeGraphWithS3.upload(self)
end

def delete_from_s3
SyncEnvelopeGraphWithS3.remove(self)
end
end
57 changes: 57 additions & 0 deletions app/services/sync_envelope_graph_with_s3.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Uploads or deletes an envelope graph from the S3 bucket
class SyncEnvelopeGraphWithS3
attr_reader :envelope

delegate :envelope_community, :envelope_ceterms_ctid, to: :envelope

def initialize(envelope)
@envelope = envelope
end

class << self
def upload(envelope)
new(envelope).upload
end

def remove(envelope)
new(envelope).remove
end
end

def upload
return unless s3_bucket_name

s3_object.put(
body: envelope.processed_resource.to_json,
content_type: 'application/json'
)

envelope.update_column(:s3_url, s3_object.public_url)
end

def remove
return unless s3_bucket_name

s3_object.delete
end

def s3_bucket
@s3_bucket ||= s3_resource.bucket(s3_bucket_name)
end

def s3_bucket_name
ENV['ENVELOPE_GRAPHS_BUCKET'].presence
end

def s3_key
"#{envelope_community.name}/#{envelope_ceterms_ctid}.json"
end

def s3_object
@s3_object ||= s3_bucket.object(s3_key)
end

def s3_resource
@s3_resource ||= Aws::S3::Resource.new(region: ENV['AWS_REGION'].presence)
end
end
6 changes: 6 additions & 0 deletions db/migrate/20251022205617_add_s3_url_to_envelopes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddS3UrlToEnvelopes < ActiveRecord::Migration[8.0]
def change
add_column :envelopes, :s3_url, :string
add_index :envelopes, :s3_url, unique: true
end
end
11 changes: 10 additions & 1 deletion db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,8 @@ CREATE TABLE public.envelopes (
publishing_organization_id uuid,
resource_publish_type character varying,
last_verified_on date,
publication_status integer DEFAULT 0 NOT NULL
publication_status integer DEFAULT 0 NOT NULL,
s3_url character varying
);


Expand Down Expand Up @@ -1480,6 +1481,13 @@ CREATE INDEX index_envelopes_on_purged_at ON public.envelopes USING btree (purge
CREATE INDEX index_envelopes_on_resource_type ON public.envelopes USING btree (resource_type);


--
-- Name: index_envelopes_on_s3_url; Type: INDEX; Schema: public; Owner: -
--

CREATE UNIQUE INDEX index_envelopes_on_s3_url ON public.envelopes USING btree (s3_url);


--
-- Name: index_envelopes_on_top_level_object_ids; Type: INDEX; Schema: public; Owner: -
--
Expand Down Expand Up @@ -1889,6 +1897,7 @@ ALTER TABLE ONLY public.envelopes
SET search_path TO "$user", public;

INSERT INTO "schema_migrations" (version) VALUES
('20251022205617'),
('20250925025616'),
('20250922224518'),
('20250921174021'),
Expand Down
2 changes: 1 addition & 1 deletion spec/factories/envelopes.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FactoryBot.define do
factory :envelope do
envelope_ceterms_ctid { Envelope.generate_ctid }
envelope_ceterms_ctid { processed_resource[:'ceterms:ctid'] || Envelope.generate_ctid }
envelope_ctdl_type { 'ceterms:CredentialOrganization' }
envelope_type { :resource_data }
envelope_version { '0.52.0' }
Expand Down
22 changes: 3 additions & 19 deletions spec/factories/resources.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
FactoryBot.define do
factory :base_resource, class: 'Hashie::Mash' do
transient do
ctid { Envelope.generate_ctid }
provisional { false }
end

add_attribute(:'adms:status') do
'graphPublicationStatus:Provisional' if provisional
end

add_attribute(:'ceterms:ctid') { ctid }
end

factory :resource, parent: :base_resource do
Expand All @@ -19,11 +22,9 @@
factory :cer_org, parent: :base_resource do
add_attribute(:@type) { 'ceterms:CredentialOrganization' }
add_attribute(:@context) { 'http://credreg.net/ctdl/schema/context/json' }
transient { ctid { Envelope.generate_ctid } }
add_attribute(:@id) do
"http://credentialengineregistry.org/resources/#{ctid}"
end
add_attribute(:'ceterms:ctid') { ctid }
add_attribute(:'ceterms:name') { 'Test Org' }
add_attribute(:'ceterms:description') { 'Org Description' }
add_attribute(:'ceterms:subjectWebpage') { 'http://example.com/test-org' }
Expand Down Expand Up @@ -51,8 +52,6 @@
end
add_attribute(:@type) { 'ceterms:Certificate' }
add_attribute(:@context) { 'http://credreg.net/ctdl/schema/context/json' }
transient { ctid { Envelope.generate_ctid } }
add_attribute(:'ceterms:ctid') { ctid }
add_attribute(:'ceterms:name') { 'Test Cred' }
add_attribute(:'ceterms:description') { 'Test Cred Description' }
add_attribute(:'ceterms:subjectWebpage') { 'http://example.com/test-cred' }
Expand All @@ -69,34 +68,28 @@
factory :cer_ass_prof, parent: :base_resource do
add_attribute(:@type) { 'ceterms:AssessmentProfile' }
add_attribute(:@context) { 'http://credreg.net/ctdl/schema/context/json' }
transient { ctid { Envelope.generate_ctid } }
add_attribute(:@id) do
"http://credentialengineregistry.org/resources/#{ctid}"
end
add_attribute(:'ceterms:ctid') { ctid }
add_attribute(:'ceterms:name') { 'Test Assessment Profile' }
end

factory :cer_cond_man, parent: :base_resource do
add_attribute(:@type) { 'ceterms:ConditionManifest' }
add_attribute(:@context) { 'http://credreg.net/ctdl/schema/context/json' }
transient { ctid { Envelope.generate_ctid } }
add_attribute(:@id) do
"http://credentialengineregistry.org/resources/#{ctid}"
end
add_attribute(:'ceterms:ctid') { ctid }
add_attribute(:'ceterms:name') { 'Test Cond Man' }
add_attribute(:'ceterms:conditionManifestOf') { [{ '@id' => 'AgentID' }] }
end

factory :cer_cost_man, parent: :base_resource do
add_attribute(:@type) { 'ceterms:CostManifest' }
add_attribute(:@context) { 'http://credreg.net/ctdl/schema/context/json' }
transient { ctid { Envelope.generate_ctid } }
add_attribute(:@id) do
"http://credentialengineregistry.org/resources/#{ctid}"
end
add_attribute(:'ceterms:ctid') { ctid }
add_attribute(:'ceterms:name') { 'Test Cost Man' }
add_attribute(:'ceterms:costDetails') { 'CostDetails' }
add_attribute(:'ceterms:costManifestOf') { [{ '@id' => 'AgentID' }] }
Expand All @@ -105,11 +98,9 @@
factory :cer_lrn_opp_prof, parent: :base_resource do
add_attribute(:@type) { 'ceterms:CostManifest' }
add_attribute(:@context) { 'http://credreg.net/ctdl/schema/context/json' }
transient { ctid { Envelope.generate_ctid } }
add_attribute(:@id) do
"http://credentialengineregistry.org/resources/#{ctid}"
end
add_attribute(:'ceterms:ctid') { ctid }
add_attribute(:'ceterms:name') { 'Test Lrn Opp Prof' }
add_attribute(:'ceterms:costDetails') { 'CostDetails' }
add_attribute(:'ceterms:costManifestOf') { [{ '@id' => 'AgentID' }] }
Expand Down Expand Up @@ -141,37 +132,31 @@
add_attribute(:@id) { ctid }
add_attribute(:@type) { 'ceterms:AssessmentProfile' }
add_attribute(:@context) { 'http://credreg.net/ctdl/schema/context/json' }
add_attribute(:'ceterms:ctid') { ctid }
add_attribute(:'ceterms:name') { 'Test Assessment Profile' }
add_attribute(:'ceasn:isPartOf') { part_of }
end

factory :cer_competency, parent: :base_resource do
transient { part_of { nil } }
transient { competency_text { 'This is the competency text...' } }
transient { ctid { Envelope.generate_ctid } }
id { "http://credentialengineregistry.org/resources/#{ctid}" }
add_attribute(:@id) { id }
add_attribute(:@type) { 'ceasn:Competency' }
add_attribute(:'ceterms:ctid') { ctid }
add_attribute(:'ceasn:isPartOf') { part_of }
add_attribute(:'ceasn:inLanguage') { ['en'] }
add_attribute(:'ceasn:competencyText') { { 'en-us' => competency_text } }
end

factory :cer_competency_framework, parent: :base_resource do
transient { ctid { Envelope.generate_ctid } }
id { "http://credentialengineregistry.org/resources/#{ctid}" }
add_attribute(:@id) { id }
add_attribute(:@type) { 'ceasn:CompetencyFramework' }
add_attribute(:'ceterms:ctid') { ctid }
add_attribute(:'ceasn:inLanguage') { ['en'] }
add_attribute(:'ceasn:name') { { 'en-us' => 'Competency Framework name' } }
add_attribute(:'ceasn:description') { { 'en-us' => 'Competency Framework description' } }
end

factory :cer_graph_competency_framework, parent: :base_resource do
transient { ctid { Envelope.generate_ctid } }
id { "http://credentialengineregistry.org/resources/#{ctid}" }
add_attribute(:@id) { id }
add_attribute(:@type) { 'ceasn:CompetencyFramework' }
Expand All @@ -186,6 +171,5 @@
attributes_for(:cer_competency_framework, ctid: ctid)
]
end
add_attribute(:'ceterms:ctid') { ctid }
end
end
Loading
Loading