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

Add domain info to db #22

Merged
merged 10 commits into from
Sep 6, 2024
Merged
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: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"forwardPorts": [3000, 5432, 8200],

// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "bundle install && rake vault:setup",
"postCreateCommand": "bundle install && rake db:setup && rake vault:setup",

// Configure tool-specific properties.
// "customizations": {},
Expand Down
8 changes: 4 additions & 4 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@ services:
build:
context: ..
dockerfile: .devcontainer/Dockerfile

volumes:
- ../..:/workspaces:cached

ports:
- 3000:3000
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity

# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
networks:
astral:
ipv4_address: "10.1.10.200"

environment:
VAULT_ADDR: http://10.1.10.100:8200
VAULT_TOKEN: root_token
Expand All @@ -25,6 +23,8 @@ services:
vault:
image: hashicorp/vault:latest
restart: unless-stopped
ports:
- 8200:8200
environment:
VAULT_DEV_ROOT_TOKEN_ID: root_token
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class ApplicationController < ActionController::API
before_action :set_default_format
rescue_from StandardError, with: :handle_standard_error
rescue_from AuthError, with: :handle_auth_error
rescue_from BadRequestError, with: :handle_bad_request_error
rescue_from ActionController::ParameterMissing, with: :handle_bad_request

attr_reader :identity # decoded and verified JWT
Expand Down Expand Up @@ -29,7 +30,7 @@ def handle_auth_error(exception)
render json: { error: "Unauthorized" }, status: :unauthorized
end

def handle_bad_request(exception)
render json: { error: exception }, status: :bad_request
def handle_bad_request_error(exception)
render json: { error: exception.message }, status: :bad_request
end
end
5 changes: 2 additions & 3 deletions app/controllers/certificates_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ class CertificatesController < ApplicationController
def create
req = CertIssueRequest.new(params_permitted)
if !req.valid?
render json: { error: req.errors }, status: :bad_request
return
raise BadRequestError.new req.errors.full_messages
end
result = IssueCert.call(request: req)
result = IssueCert.call(request: req, identity: @identity)
if result.failure?
raise StandardError.new result.message
end
Expand Down
7 changes: 7 additions & 0 deletions app/interactors/authorize_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AuthorizeRequest
include Interactor

def call
Services::DomainOwnershipService.new.authorize!(context.identity, context.request)
end
end
2 changes: 1 addition & 1 deletion app/interactors/issue_cert.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class IssueCert
include Interactor::Organizer

organize CheckPolicy, ObtainCert, Log
organize AuthorizeRequest, ObtainCert, Log
end
3 changes: 3 additions & 0 deletions app/lib/bad_request_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Error representing a bad request
class BadRequestError < StandardError
end
15 changes: 5 additions & 10 deletions app/lib/services/domain_ownership_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@ module Services
class DomainOwnershipService
def authorize!(identity, cert_req)
cert_req.fqdns.each do |fqdn|
domain = get_domain_name(fqdn)
raise AuthError unless domain.owner == identity.subject ||
(domain.group_delegation &&
(domain.groups & identity.groups).any?)
domain = Domain.where(fqdn: fqdn).first
raise AuthError unless domain.present? &&
(domain.owner == identity.subject ||
(domain.group_delegation &&
(domain.groups & identity.groups).any?))
end
nil
end

private

def get_domain_name(fqdn)
# TODO implement
end
end
end
10 changes: 10 additions & 0 deletions app/models/domain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Domain < ApplicationRecord
serialize :groups, coder: YAML, type: Array
before_save :clean_groups

validates :fqdn, :owner, presence: true

def clean_groups
self.groups = groups.sort.uniq
end
end
8 changes: 0 additions & 8 deletions app/models/domain_info.rb

This file was deleted.

12 changes: 12 additions & 0 deletions db/migrate/20240904175652_create_domains.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateDomains < ActiveRecord::Migration[7.2]
def change
create_table :domains do |t|
t.string :fqdn, null: false
t.string :owner, null: false
t.text :groups
t.boolean :group_delegation, default: false
t.timestamps
t.index :fqdn, unique: true
end
end
end
23 changes: 23 additions & 0 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
# MovieGenre.find_or_create_by!(name: genre_name)
# end

# this seed is for development only
if Rails.env.development?
Domain.first_or_create!(fqdn: "example.com", owner: "[email protected]")
end
18 changes: 18 additions & 0 deletions test/fixtures/domains.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
owner_match:
fqdn: example.com
owner: [email protected]
group_delegation: false

group_match:
fqdn: example2.com
owner: [email protected]
group_delegation: true
groups:
- "group1"

no_match:
fqdn: example3.com
owner: [email protected]
group_delegation: true
groups:
- "group3"
25 changes: 24 additions & 1 deletion test/integration/certificates_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class CertificatesControllerTest < ActionDispatch::IntegrationTest
assert_response :unauthorized
end

test "#create authorized" do
test "#create authorized as owner" do
jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZ3JvdXBzIjpbImdyb3VwMSIsImdyb3VwMiJdLCJhdWQiOiJhc3RyYWwifQ.tfRLXmE_eq-piP88_clwPWrYfMAQbCJAeZQI6OFxZSI"
post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" },
params: { cert_issue_request: { common_name: "example.com" } }
Expand All @@ -27,4 +27,27 @@ class CertificatesControllerTest < ActionDispatch::IntegrationTest
assert_includes response.parsed_body.keys, key
end
end

test "#create authorized by group" do
jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZ3JvdXBzIjpbImdyb3VwMSIsImdyb3VwMiJdLCJhdWQiOiJhc3RyYWwifQ.tfRLXmE_eq-piP88_clwPWrYfMAQbCJAeZQI6OFxZSI"
post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" },
params: { cert_issue_request: { common_name: "example2.com" } }
assert_response :success
%w[ ca_chain
certificate
expiration
issuing_ca
private_key
private_key_type
serial_number ].each do |key|
assert_includes response.parsed_body.keys, key
end
end

test "#create not authorized by group" do
jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZ3JvdXBzIjpbImdyb3VwMSIsImdyb3VwMiJdLCJhdWQiOiJhc3RyYWwifQ.tfRLXmE_eq-piP88_clwPWrYfMAQbCJAeZQI6OFxZSI"
post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" },
params: { cert_issue_request: { common_name: "example3.com" } }
assert_response :unauthorized
end
end
38 changes: 14 additions & 24 deletions test/lib/services/domain_ownership_service_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,34 @@

class DomainOwnershipServiceTest < ActiveSupport::TestCase
def setup
@identity = Identity.new(subject: "[email protected]", groups: [ "admin_group" ])
@domain = DomainInfo.new(owner: "[email protected]", group_delegation: false, groups: [ "admin_group" ])
@domain = domains(:group_match)
@identity = Identity.new(subject: @domain.owner)
@cr = CertIssueRequest.new(common_name: @domain.fqdn)
@ds = Services::DomainOwnershipService.new
end

test "#authorize! with matching owner" do
ds = Services::DomainOwnershipService.new
ds.stub :get_domain_name, @domain do
assert_nil(ds.authorize!(@identity, CertIssueRequest.new))
end
assert_nil(@ds.authorize!(@identity, @cr))
end

test "#authorize! with non-matching owner" do
ds = Services::DomainOwnershipService.new
@domain.owner = "[email protected]"
ds.stub :get_domain_name, @domain do
assert_raises(AuthError) do
ds.authorize!(@identity, CertIssueRequest.new)
end
@identity.subject = "[email protected]"
assert_raises(AuthError) do
@ds.authorize!(@identity, @cr)
end
end

test "#authorize! with matching group" do
ds = Services::DomainOwnershipService.new
@domain.owner = "[email protected]"
@domain.group_delegation = true
ds.stub :get_domain_name, @domain do
assert_nil(ds.authorize!(@identity, CertIssueRequest.new))
end
@domain.update(owner: "[email protected]")
@identity.groups = @domain.groups
assert_nil(@ds.authorize!(@identity, @cr))
end

test "#authorize! with non-matching group" do
ds = Services::DomainOwnershipService.new
@domain.owner = "[email protected]"
@domain.update(owner: "[email protected]")
@identity.groups = [ "different_group" ]
ds.stub :get_domain_name, @domain do
assert_raises(AuthError) do
ds.authorize!(@identity, CertIssueRequest.new)
end
assert_raises(AuthError) do
@ds.authorize!(@identity, @cr)
end
end
end
40 changes: 40 additions & 0 deletions test/models/domain_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# test/models/cert_issue_request_test.rb
require "test_helper"

class DomainTest < ActiveSupport::TestCase
def setup
@attributes = {
fqdn: "example4.com",
owner: "[email protected]"
}
@domain = Domain.new(@attributes)
end

test "#new should set attributes from attributes argument" do
@attributes.each do |key, value|
assert_equal value, @domain.send(key), "Attribute #{key} was not set correctly"
end
end

test "#valid? should be valid with valid attributes" do
assert @domain.valid?
end

test "#valid? should require an fqdn" do
@domain.fqdn = nil
assert_not @domain.valid?
assert_includes @domain.errors[:fqdn], "can't be blank"
end

test "#valid? should require an owner" do
@domain.owner = nil
assert_not @domain.valid?
assert_includes @domain.errors[:owner], "can't be blank"
end

test "before_save should sort and dedupe groups" do
@domain.groups = [ "two", "two", "one" ]
@domain.save
assert_equal [ "one", "two" ], @domain.groups
end
end