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

App registry step #24

Merged
merged 15 commits into from
Sep 13, 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
18 changes: 18 additions & 0 deletions .devcontainer/app_reg_db.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"domain-names": [
{
"id": "example.com",
"fullyQualifiedDomainName": "example.com",
"ownerDelegatedRequestsToTeam": true,
"autoApprovedGroups": "group1",
"autoApprovedServiceAccounts": "[email protected]"
},
{
"id": "example2.com",
"fullyQualifiedDomainName": "example2.com",
"ownerDelegatedRequestsToTeam": true,
"autoApprovedGroups": "group1",
"autoApprovedServiceAccounts": "[email protected]"
}
]
}
3 changes: 3 additions & 0 deletions .devcontainer/app_reg_routes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"/api/v1beta1/*": "/$1"
}
16 changes: 16 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ services:
VAULT_ADDR: http://10.1.10.100:8200
VAULT_TOKEN: root_token
JWT_SIGNING_KEY: jwt_secret
APP_REGISTRY_ADDR: http://10.1.10.150:8800
APP_REGISTRY_TOKEN: app_reg_token

vault:
image: hashicorp/vault:latest
Expand All @@ -32,6 +34,20 @@ services:
astral:
ipv4_address: "10.1.10.100"

app_registry:
image: node:latest
restart: unless-stopped
ports:
- 8800:8800
volumes:
- .:/data
networks:
astral:
ipv4_address: "10.1.10.150"
command: >
sh -c "npm install -g [email protected] &&
json-server /data/app_reg_db.json --routes /data/app_reg_routes.json --port 8800 --host 0.0.0.0"

networks:
astral:
ipam:
Expand Down
9 changes: 6 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ gem "rails", "~> 7.2.1"
gem "sqlite3", ">= 1.4"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
# gem "jbuilder"

# Use Redis adapter to run Action Cable in production
# gem "redis", ">= 4.0.1"

Expand Down Expand Up @@ -38,9 +37,13 @@ gem "vault"
# Use the jwt gem to decode access tokens
gem "jwt"

# Use the jbuilder gem
# Use the jbuilder gem to render JSON views
gem "jbuilder"

# Use the faraday gem for http client operations
gem "faraday"
gem "faraday-retry"

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri mswin ], require: "debug/prelude"
Expand Down
12 changes: 12 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ GEM
reline (>= 0.3.8)
drb (2.2.1)
erubi (1.13.0)
faraday (2.11.0)
faraday-net_http (>= 2.0, < 3.4)
logger
faraday-net_http (3.3.0)
net-http
faraday-retry (2.2.1)
faraday (~> 2.0)
globalid (1.2.1)
activesupport (>= 6.1)
i18n (1.14.5)
Expand Down Expand Up @@ -120,6 +127,8 @@ GEM
mini_mime (1.1.5)
minitest (5.25.1)
msgpack (1.7.2)
net-http (0.4.1)
uri
net-imap (0.4.14)
date
net-protocol
Expand Down Expand Up @@ -245,6 +254,7 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
uri (0.13.1)
useragent (0.16.10)
vault (0.18.2)
aws-sigv4
Expand Down Expand Up @@ -274,6 +284,8 @@ DEPENDENCIES
bootsnap
brakeman
debug
faraday
faraday-retry
interactor (~> 3.0)
jbuilder
jwt
Expand Down
2 changes: 1 addition & 1 deletion app/interactors/authenticate_identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class AuthenticateIdentity
end

def call
if identity = Services::AuthService.new.authenticate!(context.token)
if identity = Services::AuthService.authenticate!(context.token)
context.identity = identity
else
context.fail!(message: "Invalid token")
Expand Down
9 changes: 8 additions & 1 deletion app/interactors/authorize_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ class AuthorizeRequest
include FailOnError

def call
Services::DomainOwnershipService.new.authorize!(context.identity, context.request)
context.request.fqdns.each do |fqdn|
suprjinx marked this conversation as resolved.
Show resolved Hide resolved
domain = Domain.where(fqdn: fqdn).first
raise AuthError.new("Common or alt name not recognized") unless domain
raise AuthError.new("No subject or group authorization") unless
domain.users_array.include?(context.identity.subject) ||
(domain.group_delegation? && (domain.groups_array & context.identity.groups).any?)
end
nil
end
end
3 changes: 3 additions & 0 deletions app/interactors/fail_on_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ module FailOnError
included do
around do |interactor|
interactor.call
rescue Interactor::Failure => e
raise e
rescue => e
Rails.logger.error("Error in #{self.class.name}: #{e.class.name} - #{e.message}")
context.fail!(error: e)
end
end
Expand Down
3 changes: 2 additions & 1 deletion app/interactors/issue_cert.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class IssueCert
include Interactor::Organizer
include FailOnError

organize AuthorizeRequest, ObtainCert, Log
organize RefreshDomain, AuthorizeRequest, ObtainCert, Log
end
2 changes: 1 addition & 1 deletion app/interactors/obtain_cert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class ObtainCert
include FailOnError

def call
if cert = Services::CertificateService.new.issue_cert(context.request)
if cert = Services::CertificateService.issue_cert(context.request)
context.cert = cert
else
context.fail!(message: "Failed to issue certificate")
Expand Down
20 changes: 20 additions & 0 deletions app/interactors/refresh_domain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class RefreshDomain
include Interactor

def call
domain_info = Services::DomainOwnershipService.get_domain_info(context.request.common_name)
domain_record = Domain.find_or_create_by!(fqdn: context.request.common_name)
if !domain_info
domain_record.destroy!
return
end

domain_record.update!(
group_delegation: domain_info.group_delegation,
groups: domain_info.groups,
users: domain_info.users
)
rescue => e
Rails.logger.warn("Continuing after error in #{self.class.name}: #{e.class.name}: #{e.message}")
end
end
53 changes: 53 additions & 0 deletions app/lib/services/app_registry_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module Services
class AppRegistryService
class << self
def get_domain_info(fqdn)
rslt = client.get("/api/v1beta1/domain-names/#{fqdn}").body
convert(rslt)
rescue Faraday::ResourceNotFound => e
nil
end

private

def client
Faraday.new(ssl: ssl_opts, url: Rails.configuration.astral[:app_registry_addr]) do |faraday|
faraday.request :authorization, "Bearer", -> { Rails.configuration.astral[:app_registry_token] }
faraday.request :retry, retry_opts
faraday.response :json
faraday.response :raise_error, include_request: true
end
end

def convert(domain_info)
if !domain_info || domain_info["isDeleted"]
return nil
end

OpenStruct.new(
fqdn: domain_info["fullyQualifiedDomainName"],
group_delegation: domain_info["ownerDelegatedRequestsToTeam"],
groups: domain_info["autoApprovedGroups"],
users: domain_info["autoApprovedServiceAccounts"]
)
end

def ssl_opts
{
ca_file: Rails.configuration.astral[:app_registry_ca_file],
client_cert: Rails.configuration.astral[:app_registry_client_cert],
client_key: Rails.configuration.astral[:app_registry_client_key]
}
end

def retry_opts
{
max: 3,
interval: 0.05,
interval_randomness: 0.5,
backoff_factor: 2
}
end
end
end
end
38 changes: 16 additions & 22 deletions app/lib/services/auth_service.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
module Services
class AuthService
def initialize
@domain_ownership_service = DomainOwnershipService.new
end

def authenticate!(token)
identity = decode(token)
raise AuthError unless identity
# TODO verify identity with authority?
identity
end

def authorize!(identity, cert_issue_req)
@domain_ownership_service.authorize!(identity, cert_issue_req)
end
class << self
def authenticate!(token)
identity = decode(token)
raise AuthError unless identity
# TODO verify identity with authority?
identity
end

private
private

def decode(token)
# Decode a JWT access token using the configured base.
body = JWT.decode(token, Rails.application.config.astral[:jwt_signing_key])[0]
Identity.new(body)
rescue => e
Rails.logger.warn "Unable to decode token: #{e}"
nil
def decode(token)
# Decode a JWT access token using the configured base.
body = JWT.decode(token, Rails.configuration.astral[:jwt_signing_key])[0]
Identity.new(body)
rescue => e
Rails.logger.warn "Unable to decode token: #{e}"
nil
end
end
end
end
16 changes: 10 additions & 6 deletions app/lib/services/certificate_service.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
module Services
class CertificateService
def initialize
# TODO this should select an implementation service based on config
@impl = VaultService.new
end
class << self
def issue_cert(cert_issue_request)
impl.issue_cert(cert_issue_request)
end

private

def issue_cert(cert_issue_request)
@impl.issue_cert(cert_issue_request)
def impl
# TODO this should select an implementation service based on config
VaultService
end
end
end
end
18 changes: 10 additions & 8 deletions app/lib/services/domain_ownership_service.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
module Services
class DomainOwnershipService
def authorize!(identity, cert_req)
cert_req.fqdns.each do |fqdn|
domain = Domain.where(fqdn: fqdn).first
raise AuthError unless domain.present? &&
(domain.owner == identity.subject ||
(domain.group_delegation &&
(domain.groups & identity.groups).any?))
class << self
def get_domain_info(fqdn)
impl.get_domain_info(fqdn)
end

private

def impl
# TODO this should select an implementation service based on config
AppRegistryService
end
nil
end
end
end
28 changes: 16 additions & 12 deletions app/lib/services/vault_service.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
module Services
class VaultService
def initialize
# TODO create a new token for use in the session
@client = Vault::Client.new(
address: Rails.application.config.astral[:vault_addr],
token: Rails.application.config.astral[:vault_token]
)
end
class << self
def issue_cert(cert_issue_request)
opts = cert_issue_request.attributes
# Generate the TLS certificate using the intermediate CA
tls_cert = client.logical.write(Rails.configuration.astral[:vault_cert_path], opts)
OpenStruct.new tls_cert.data
end

private

def issue_cert(cert_issue_request)
opts = cert_issue_request.attributes
# Generate the TLS certificate using the intermediate CA
tls_cert = @client.logical.write(Rails.application.config.astral[:vault_cert_path], opts)
OpenStruct.new tls_cert.data
def client
# TODO create a new token for use in the session
Vault::Client.new(
address: Rails.configuration.astral[:vault_addr],
token: Rails.configuration.astral[:vault_token]
)
end
end
end
end
11 changes: 6 additions & 5 deletions app/models/domain.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
class Domain < ApplicationRecord
serialize :groups, coder: YAML, type: Array
before_save :clean_groups
validates :fqdn, presence: true

validates :fqdn, :owner, presence: true
def groups_array
(groups || "").split(",").sort.uniq
end

def clean_groups
self.groups = groups.sort.uniq
def users_array
(users || "").split(",").sort.uniq
end
end
Loading