Skip to content
This repository has been archived by the owner on Oct 8, 2021. It is now read-only.

Commit

Permalink
[wip] OPA authorization
Browse files Browse the repository at this point in the history
  • Loading branch information
guicassolato committed Oct 27, 2020
1 parent 95ed847 commit 7a11df8
Show file tree
Hide file tree
Showing 14 changed files with 336 additions and 34 deletions.
1 change: 1 addition & 0 deletions auth-ruby/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.ruby-version
13 changes: 9 additions & 4 deletions auth-ruby/examples/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ localhost:8000:

authorization:
- opa:
endpoint: http://localhost:8181/v1/data/ostia/authz/localhost
rego: |
allow {
input.method == "PUT"
some petid
input.path = ["pets", petid]
input.user == input.owner
input.method == "GET"
input.path = ["pets"]
}
allow {
input.method == "POST"
input.path = ["pets"]
input.context.identity.roles[_] == "admin"
}
- jwt:
match:
Expand Down
9 changes: 9 additions & 0 deletions auth-ruby/examples/plans.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
localhost:8000:
basic:
disabled_endpoints:
- method: PUT
path: "/pets/{id}" # FIXME
- method: GET
path: "/pets/stats"
enterprise:
disabled_endpoints: []
52 changes: 52 additions & 0 deletions auth-ruby/examples/policies/localhost.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package ostia.authz.localhost

import input.attributes.request.http as http_request
import input.context.identity
import input.context.metadata

resource = object.get(input.context, "resource", {})
path_arr = split(trim_left(http_request.path, "/"), "/")

default allow = false

allow {
http_request.method == "GET"
path_arr = ["pets"]
}

allow {
http_request.method == "POST"
path_arr = ["pets"]
}

allow {
http_request.method == "GET"
own_resource
}

allow {
http_request.method == "PUT"
own_resource
}

allow {
http_request.method == "DELETE"
own_resource
}

allow {
http_request.method == "GET"
path_arr = ["pets", "stats"]
is_admin
}

own_resource {
some petid
path_arr = ["pets", petid]
subject := object.get(identity, "sub", object.get(identity, "username", ""))
subject == object.get(resource, "owner", "")
}

is_admin {
metadata.user_info.roles[_] == "admin"
}
16 changes: 14 additions & 2 deletions auth-ruby/src/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,28 +48,40 @@ def initialize(config)
rpc :Check, Envoy::Service::Auth::V2::CheckRequest, Envoy::Service::Auth::V2::CheckResponse

class Context
attr_reader :identity, :metadata
attr_reader :identity, :metadata, :authorization
attr_reader :request, :service

def initialize(request, service)
@request = request
@service = service
@identity = {}
@metadata = {}
@authorization = {}
end

def evaluate!
proc = ->(obj, result) { result[obj] = obj.call(self) }

service.identity.each_with_object(identity, &proc)
service.metadata.each_with_object(metadata, &proc)
service.authorization.each_with_object(authorization, &proc)

@identity.freeze
@metadata.freeze
@authorization.freeze
end

def valid?
identity.values.any?
identity.values.any? && authorization.values.all?(&:authorized?)
end

def to_h
{
request: request,
service: service,
identity: identity.transform_keys(&:name).transform_values(&:to_h),
metadata: metadata.transform_keys{ |key| key.class.to_s.demodulize.underscore }
}.transform_values(&:to_h)
end
end

Expand Down
46 changes: 46 additions & 0 deletions auth-ruby/src/config/authorization.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,56 @@
# frozen_string_literal: true

require 'net/http'
require 'uri'
require 'json'

class Config::Authorization < OpenStruct
extend Config::BuildSubclass

class Response < OpenStruct
def authorized?
raise NotImplementedError, __method__
end
end

class OPA < self
class Response < Config::Authorization::Response
def authorized?
result['allow']
end
end

def call(context)
uri = URI.parse(endpoint)
auth_request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
request, identity, metadata = context.to_h.values_at(:request, :identity, :metadata)
auth_request.body = { input: request.merge(context: { identity: identity.values.first, metadata: metadata }) }.to_json
auth_response = Net::HTTP.start(uri.hostname, uri.port) do |http|
http.request(auth_request)
end

case auth_response
when Net::HTTPOK
response_json = case auth_response['content-type']
when 'application/json'
JSON.parse(auth_response.body)
else
{ allowed: true, message: auth_response.body }
end
Response.new(response_json)
end
end
end

class JWT < self
class Response < Config::Authorization::Response
def authorized?
true
end
end

def call(context)
Response.new # TODO
end
end
end
3 changes: 3 additions & 0 deletions auth-ruby/src/config/identity/oidc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def to_s
@token
end

delegate :as_json, to: :@decoded, allow_nil: true
alias to_h as_json

private def method_missing(symbol, *args, &block)
return super unless @decoded
@decoded.public_send(symbol, *args, &block)
Expand Down
32 changes: 32 additions & 0 deletions auth-ruby/test/config/test_authorization.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

require 'minitest/autorun'
require_relative '../test_helper'
require 'json'

describe Config::Authorization do
let(:authorization_config) { YAML.load(file_fixture('config.yml').read).dig('localhost:8000', 'authorization') }

describe 'opa' do
let(:described_class) { Config::Authorization::OPA }
let(:input) { JSON.parse(file_fixture('opa_input.json').read)['input'] }
let(:config) { authorization_config.first['opa'] }
let(:context) do
OpenStruct.new(to_h: {
request: input.slice('attributes'),
identity: { 'test' => input.dig('context', 'identity') },
metadata: input.dig('context', 'metadata')
})
end

subject do
described_class.new(config)
end

it 'wraps the response' do
result = subject.call(context)
expect(result).must_be_instance_of(described_class::Response)
refute(result.authorized?) # user does not have permission
end
end
end
13 changes: 9 additions & 4 deletions auth-ruby/test/fixtures/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,17 @@ localhost:8000:

authorization:
- opa:
endpoint: http://localhost:8181/v1/data/ostia/authz/localhost
rego: |
allow {
input.method == "PUT"
some petid
input.path = ["pets", petid]
input.user == input.owner
input.method == "GET"
input.path = ["pets"]
}
allow {
input.method == "POST"
input.path = ["pets"]
input.context.identity.roles[_] == "admin"
}
- jwt:
match:
Expand Down
81 changes: 81 additions & 0 deletions auth-ruby/test/fixtures/opa_input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"input": {
"attributes": {
"source": {
"address": {
"socket_address": {
"protocol": "TCP",
"address": "172.18.0.1",
"port_value": 47428,
"named_port": "",
"resolver_name": "",
"ipv4_compat": false
},
"pipe": null
},
"service": "",
"labels": {},
"principal": "",
"certificate": ""
},
"destination": {
"address": null,
"service": "",
"labels": {},
"principal": "",
"certificate": ""
},
"request": {
"time": {
"seconds": 1603367724,
"nanos": 500287999
},
"http": {
"id": "8911960713991399467",
"method": "GET",
"headers": {
":path": "/pets/stats",
":method": "GET",
"x-request-id": "7a9bc5dd-1672-43ca-a99f-e209b8c725ca",
":authority": "localhost:8000",
"user-agent": "curl/7.70.0",
"x-forwarded-proto": "http",
"accept": "*/*",
"authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJQaUVxSGRuUklEUTFSSlVkMC0xdllsM1RVbHp6ZHg0UnVJSlZVMUtwc2dVIn0.eyJleHAiOjE2MDM3MjQ0MzMsImlhdCI6MTYwMzcyNDEzMywianRpIjoiMDgwYTMwMTQtZjdkOC00M2RmLWFlZmMtNDc3NDcwNDE2NTQ5IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiODg4NTZlNWQtMGEzOS00YzE4LTljYjktMWM2NjJlYWFhMDgwIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoib3N0aWEiLCJzZXNzaW9uX3N0YXRlIjoiMjhjNTljMDUtZDBjOC00YWZjLWI4OWYtZDY5YmEyOGVhOTU5IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJjbGllbnRJZCI6Im9zdGlhIiwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtb3N0aWEiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSJ9.T3CUaX1rqN1qY1wSAeaGxcvc1691cDSWDyKpTXeI1SjsZPMbzPC_pFujxBbk-FTIGlmisr7JEFXuAFsSeHfgoDFQIHoZ9vZP_fhoioLKtBOxB6VgrRT24CDqXi08AGAjPnw86J8Pu2AHwjE0Le1V6b9DXBA155vwaHQ5TbT6XOVHocz7gds3xKsaS_vGVDlFOFArr9F0CsxAilKyfvMeu077025FvB41ICXihumev87qBKhcLrx3ZpQ7A5fNQuMIRttVP_cRP23bayeBBjqq3kfvqgNALveqbH4ymcW-bWsYTEw6gkecm9XS0bOK6tKtdythnE2al0E7uXLwyOedtg"
},
"path": "/pets/stats",
"host": "localhost:8000",
"scheme": "",
"query": "",
"fragment": "",
"size": 0,
"protocol": "HTTP/1.1",
"body": ""
}
},
"context_extensions": {},
"metadata_context": {
"filter_metadata": {}
}
},
"context": {
"identity": {
"iss": "http://localhost:8080/auth/realms/test",
"sub": "88856e5d-0a39-4c18-9cb9-1c662eaaa080",
"aud": "account",
"exp": 1603724433,
"iat": 1603724133,
"acr": "1",
"azp": "ostia",
"jti": "080a3014-f7d8-43df-aefc-477470416549"
},
"metadata": {
"user_info": {
"roles": [
"member"
]
}
}
}
}
}
49 changes: 49 additions & 0 deletions auth-ruby/test/fixtures/request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"attributes": {
"source": {
"address": {
"socketAddress": {
"protocol": "TCP",
"address": "172.18.0.1",
"portValue": 47428,
"resolverName": "",
"ipv4Compat": false
}
},
"service": "",
"labels": {},
"principal": "",
"certificate": ""
},
"destination": {},
"request": {
"time": "2020-10-22T11:55:24.500288000Z",
"http": {
"id": "8911960713991399467",
"method": "GET",
"headers": {
":path": "/pets",
":method": "GET",
":authority": "localhost:8000",
"x-request-id": "7a9bc5dd-1672-43ca-a99f-e209b8c725ca",
"user-agent": "curl/7.70.0",
"x-forwarded-proto": "http",
"authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJQaUVxSGRuUklEUTFSSlVkMC0xdllsM1RVbHp6ZHg0UnVJSlZVMUtwc2dVIn0.eyJleHAiOjE2MDM3MjQ0MzMsImlhdCI6MTYwMzcyNDEzMywianRpIjoiMDgwYTMwMTQtZjdkOC00M2RmLWFlZmMtNDc3NDcwNDE2NTQ5IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiODg4NTZlNWQtMGEzOS00YzE4LTljYjktMWM2NjJlYWFhMDgwIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoib3N0aWEiLCJzZXNzaW9uX3N0YXRlIjoiMjhjNTljMDUtZDBjOC00YWZjLWI4OWYtZDY5YmEyOGVhOTU5IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJjbGllbnRJZCI6Im9zdGlhIiwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtb3N0aWEiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSJ9.T3CUaX1rqN1qY1wSAeaGxcvc1691cDSWDyKpTXeI1SjsZPMbzPC_pFujxBbk-FTIGlmisr7JEFXuAFsSeHfgoDFQIHoZ9vZP_fhoioLKtBOxB6VgrRT24CDqXi08AGAjPnw86J8Pu2AHwjE0Le1V6b9DXBA155vwaHQ5TbT6XOVHocz7gds3xKsaS_vGVDlFOFArr9F0CsxAilKyfvMeu077025FvB41ICXihumev87qBKhcLrx3ZpQ7A5fNQuMIRttVP_cRP23bayeBBjqq3kfvqgNALveqbH4ymcW-bWsYTEw6gkecm9XS0bOK6tKtdythnE2al0E7uXLwyOedtg",
"accept": "*/*"
},
"path": "/pets",
"host": "localhost:8000",
"scheme": "",
"query": "",
"fragment": "",
"size": "0",
"protocol": "HTTP/1.1",
"body": ""
}
},
"contextExtensions": {},
"metadataContext": {
"filterMetadata": {}
}
}
}
Loading

0 comments on commit 7a11df8

Please sign in to comment.