summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormr <mr@mrautenberg.de>2010-12-06 12:09:06 +0100
committermr <mr@mrautenberg.de>2010-12-06 12:09:06 +0100
commitbb99bb49636db1d3f07b6f540dc8624a677ade2f (patch)
tree325c99a3781febf755a4bebc275d891fa1b9937c
parent351694975c611999856c722d8cc3ae971811bc7b (diff)
insert basic a&a libs to development branch
-rw-r--r--lib/authorization.rb291
-rw-r--r--lib/environment.rb2
-rw-r--r--lib/helper.rb72
-rw-r--r--lib/opentox-ruby.rb2
-rw-r--r--lib/policy.rb242
5 files changed, 592 insertions, 17 deletions
diff --git a/lib/authorization.rb b/lib/authorization.rb
new file mode 100644
index 0000000..0cba96a
--- /dev/null
+++ b/lib/authorization.rb
@@ -0,0 +1,291 @@
+module OpenTox
+
+ #Module for Authorization and Authentication
+ #@example Authentication
+ # require "opentox-ruby-api-wrapper"
+ # OpenTox::Authorization::AA_SERVER = "https://opensso.in-silico.ch" #if not set in .opentox/conf/[environment].yaml
+ # token = OpenTox::Authorization.authenticate("benutzer", "passwort")
+ #@see http://www.opentox.org/dev/apis/api-1.2/AA OpenTox A&A API 1.2 specification
+
+ module Authorization
+
+ #Helper Class AA to create and send default policies out of xml templates
+ #@example Creating a default policy to a URI
+ # aa=OpenTox::Authorization::AA.new(tok)
+ # xml=aa.get_xml('http://uri....')
+ # OpenTox::Authorization.create_policy(xml,tok)
+
+ class AA
+ attr_accessor :user, :token_id, :policy
+
+ #Generates AA object - requires token_id
+ # @param [String] token_id
+ def initialize(token_id)
+ @user = Authorization.get_user(token_id)
+ @token_id = token_id
+ @policy = Policies.new()
+ end
+
+ #Cleans AA Policies and loads default xml file into policy attribute
+ #set uri and user, returns Policyfile(XML) for open-sso
+ # @param [String] URI to create a policy for
+ def get_xml(uri)
+ @policy.drop_policies
+ @policy.load_default_policy(@user, uri)
+ return @policy.to_xml
+ end
+
+ #Loads and sends Policyfile(XML) to open-sso server
+ # @param [String] URI to create a policy for
+ def send(uri)
+ xml = get_xml(uri)
+ ret = false
+ ret = Authorization.create_policy(xml, @token_id)
+ LOGGER.debug "Policy send with token_id: #{@token_id}"
+ LOGGER.warn "Not created Policy is: #{xml}" if !ret
+ ret
+ end
+
+ end
+
+ #Returns the open-sso server set in the config file .opentox/config/[environment].yaml
+ # @return [String, nil] the openSSO server URI or nil
+ def self.server
+ return AA_SERVER
+ end
+
+ #Authentication against OpenSSO. Returns token. Requires Username and Password.
+ # @param [String, String]Username,Password
+ # @return [String, nil] gives token_id or nil
+ def self.authenticate(user, pw)
+ return true if !AA_SERVER
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/auth/authenticate")
+ out = resource.post(:username=>user, :password => pw).sub("token.id=","").sub("\n","")
+ return out
+ rescue
+ return nil
+ end
+ end
+
+ #Logout on opensso. Make token invalid. Requires token
+ # @param [String]token_id the token_id
+ # @return [Boolean] true if logout is OK
+ def self.logout(token_id)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/auth/logout")
+ resource.post(:subjectid => token_id)
+ return true
+ rescue
+ return false
+ end
+ end
+
+ #Authorization against OpenSSO for a URI with request-method (action) [GET/POST/PUT/DELETE]
+ # @param [String,String,String]uri,action,token_id
+ # @return [Boolean, nil] returns true, false or nil (if authorization-request fails).
+ def self.authorize(uri, action, token_id)
+ return true if !AA_SERVER
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/auth/authorize")
+ return true if resource.post(:uri => uri, :action => action, :subjectid => token_id) == "boolean=true\n"
+ rescue
+ return nil
+ end
+ end
+
+ #Checks if a token is a valid token
+ # @param [String]token_id token_id from openSSO session
+ # @return [Boolean] token_id is valid or not.
+ def self.is_token_valid(token_id)
+ return true if !AA_SERVER
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/auth/isTokenValid")
+ return true if resource.post(:tokenid => token_id) == "boolean=true\n"
+ rescue
+ return false
+ end
+ end
+
+ #Returns array with all policies of the token owner
+ # @param [String]token_id requires token_id
+ # @return [Array, nil] returns an Array of policy names or nil if request fails
+ def self.list_policies(token_id)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ out = resource.get(:subjectid => token_id)
+ return out.split("\n")
+ rescue
+ return nil
+ end
+ end
+
+ #Returns a policy in xml-format
+ # @param [String, String]policy,token_id
+ # @return [String] XML of the policy
+ def self.list_policy(policy, token_id)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ return resource.get(:subjectid => token_id,:id => policy)
+ rescue
+ return nil
+ end
+ end
+
+ #Returns the owner (who created the first policy) of an URI
+ # @param [String, String]uri,token_id
+ # return [String, nil]owner,nil returns owner of the URI
+ def self.get_uri_owner(uri, token_id)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ return resource.get(:uri => uri, :subjectid => token_id).sub("\n","")
+ rescue
+ return nil
+ end
+ end
+
+ #Checks if a policy exists to a URI. Requires URI and token.
+ # @param [String, String]uri,token_id
+ # return [Boolean]
+ def self.uri_has_policy(uri, token_id)
+ owner = get_uri_owner(uri, token_id)
+ return true if owner and owner != "null"
+ false
+ end
+
+ #List all policynames for a URI. Requires URI and token.
+ # @param [String, String]uri,token_id
+ # return [Array, nil] returns an Array of policy names or nil if request fails
+ def self.list_uri_policies(uri, token_id)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ out = resource.get(:uri => uri, :polnames => true, :subjectid => token_id)
+ policies = []; notfirstline = false
+ out.split("\n").each do |line|
+ policies << line if notfirstline
+ notfirstline = true
+ end
+ return policies
+ rescue
+ return nil
+ end
+ end
+
+ #Sends a policy in xml-format to opensso server. Requires policy-xml and token.
+ # @param [String, String]policyxml,token_id
+ # return [Boolean] returns true if policy is created
+ def self.create_policy(policy, token_id)
+ begin
+# resource = RestClient::Resource.new("#{AA_SERVER}/Pol/opensso-pol")
+ LOGGER.debug "OpenTox::Authorization.create_policy policy: #{policy[168,43]} with token:" + token_id.to_s + " length: " + token_id.length.to_s
+# return true if resource.post(policy, :subjectid => token_id, :content_type => "application/xml")
+ return true if RestClientWrapper.post("#{AA_SERVER}/pol", {:subjectid => token_id, :content_type => "application/xml"}, policy)
+ rescue
+ return false
+ end
+ end
+
+ #Deletes a policy
+ # @param [String, String]policyname,token_id
+ # @return [Boolean,nil]
+ def self.delete_policy(policy, token_id)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ LOGGER.debug "OpenTox::Authorization.delete_policy policy: #{policy} with token: #{token_id}"
+ return true if resource.delete(:subjectid => token_id, :id => policy)
+ rescue
+ return nil
+ end
+ end
+
+ #Returns array of all possible LDAP-Groups
+ # @param [String]token_id
+ # @return [Array]
+ def self.list_groups(token_id)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/opensso/identity/search")
+ grps = resource.post(:admin => token_id, :attributes_names => "objecttype", :attributes_values_objecttype => "group")
+ grps.split("\n").collect{|x| x.sub("string=","")}
+ rescue
+ []
+ end
+ end
+
+ #Returns array of the LDAP-Groups of an user
+ # @param [String]token_id
+ # @return [Array] gives array of LDAP groups of a user
+ def self.list_user_groups(user, token_id)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/opensso/identity/read")
+ out = resource.post(:name => user, :admin => token_id, :attributes_names => "group")
+ grps = []
+ out.split("\n").each do |line|
+ grps << line.sub("identitydetails.group=","") if line.include?("identitydetails.group=")
+ end
+ return grps
+ rescue
+ []
+ end
+ end
+
+ #Returns the owner (user id) of a token
+ # @param [String]token_id
+ # @return [String]user
+ def self.get_user(token_id)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/opensso/identity/attributes")
+ out = resource.post(:subjectid => token_id, :attributes_names => "uid")
+ user = ""; check = false
+ out.split("\n").each do |line|
+ if check
+ user = line.sub("userdetails.attribute.value=","") if line.include?("userdetails.attribute.value=")
+ check = false
+ end
+ check = true if line.include?("userdetails.attribute.name=uid")
+ end
+ return user
+ rescue
+ nil
+ end
+ end
+
+ #Send default policy with Authorization::AA class
+ # @param [String, String]URI,token_id
+ def self.send_policy(uri, token_id)
+ return true if !AA_SERVER
+ aa = Authorization::AA.new(token_id)
+ ret = aa.send(uri)
+ LOGGER.debug "OpenTox::Authorization send policy for URI: #{uri} | token_id: #{token_id} - policy created: #{ret}"
+ ret
+ end
+
+ #Deletes all policies of an URI
+ # @param [String, String]URI,token_id
+ # @return [Boolean]
+ def self.delete_policies_from_uri(uri, token_id)
+ policies = list_uri_policies(uri, token_id)
+ policies.each do |policy|
+ ret = delete_policy(policy, token_id)
+ LOGGER.debug "OpenTox::Authorization delete policy: #{policy} - with result: #{ret}"
+ end
+ return true
+ end
+
+ #Checks (if token_id is valid) if a policy exist and create default policy if not
+ def self.check_policy(uri, token_id)
+ token_valid = OpenTox::Authorization.is_token_valid(token_id)
+ LOGGER.debug "OpenTox::Authorization.check_policy with uri: #{uri}, token_id: #{token_id} is valid: #{token_valid}"
+ if uri and token_valid
+ if !uri_has_policy(uri, token_id)
+ return send_policy(uri, token_id)
+ else
+ LOGGER.debug "OpenTox::Authorization.check_policy URI: #{uri} has already a Policy."
+ end
+ end
+ true
+ end
+
+ end
+end
+
+
diff --git a/lib/environment.rb b/lib/environment.rb
index 4f1cc80..1761d92 100644
--- a/lib/environment.rb
+++ b/lib/environment.rb
@@ -83,6 +83,8 @@ class OwlNamespace
end
+AA_SERVER = CONFIG[:authorization] ? (CONFIG[:authorization][:server] ? CONFIG[:authorization][:server] : nil) : nil
+
RDF = OwlNamespace.new 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
OWL = OwlNamespace.new 'http://www.w3.org/2002/07/owl#'
DC = OwlNamespace.new 'http://purl.org/dc/elements/1.1/'
diff --git a/lib/helper.rb b/lib/helper.rb
index a9f451e..b69f9b4 100644
--- a/lib/helper.rb
+++ b/lib/helper.rb
@@ -1,26 +1,66 @@
helpers do
# Authentification
- def protected!
- response['WWW-Authenticate'] = %(Basic realm="Testing HTTP Auth") and \
+ def protected!(token_id)
+ if env["session"]
+ flash[:notice] = "You don't have access to this section: " and \
+ redirect back and \
+ return unless authorized?(token_id)
+ end
throw(:halt, [401, "Not authorized\n"]) and \
- return unless authorized?
+ return unless authorized?(token_id)
end
-
- def authorized?
- @auth ||= Rack::Auth::Basic::Request.new(request.env)
- @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == ['api', API_KEY]
+
+ def authorized?(token_id)
+ case request.env['REQUEST_METHOD']
+ when "DELETE", "PUT"
+ ret = OpenTox::Authorization.authorize(request.env['SCRIPT_URI'], request.env['REQUEST_METHOD'], token_id)
+ LOGGER.debug "OpenTox helpers OpenTox::Authorization authorized? method: #{request.env['REQUEST_METHOD']} , URI: #{request.env['SCRIPT_URI']}, token_id: #{token_id} with return #{ret}."
+ return ret
+ when "POST"
+ if OpenTox::Authorization.is_token_valid(token_id)
+ LOGGER.debug "OpenTox helpers OpenTox::Authorization.is_token_valid: true"
+ return true
+ end
+ LOGGER.warn "OpenTox helpers POST on #{request.env['SCRIPT_URI']} with token_id: #{token_id} false."
+ end
+ LOGGER.debug "Not authorized for: 1. #{request['SCRIPT_URI']} 2. #{request.env['SCRIPT_URI']} with Method: #{request.env['REQUEST_METHOD']} with Token #{token_id}"
+ LOGGER.debug "Request infos: #{request.inspect}"
+ return false
end
-
-=begin
- def xml(object)
- builder do |xml|
- xml.instruct!
- object.to_xml
- end
- end
-=end
+ def unprotected_requests
+ case env['REQUEST_URI']
+ when /\/login$|\/logout$|\/predict$|\/upload$/
+ return true
+ when /\/compound|\/feature|\/task|\/toxcreate/ #to fix: read from config | validation should be protected
+ return true
+ else
+ return false
+ end
+ end
+
+ def check_token_id(token_id)
+ return false if !token_id
+ return true if token_id.size > 62
+ false
+ end
+end
+before do
+
+ unless unprotected_requests or env['REQUEST_METHOD'] == "GET"
+ begin
+ token_id = session[:token_id] if session[:token_id]
+ token_id = params[:token_id] if params[:token_id] and !check_token_id(token_id)
+ token_id = request.env['HTTP_TOKEN_ID'] if request.env['HTTP_TOKEN_ID'] and !check_token_id(token_id)
+ # see http://rack.rubyforge.org/doc/SPEC.html
+ rescue
+ LOGGER.debug "OpenTox api wrapper: helper before filter: NO token_id."
+ token_id = ""
+ end
+ protected!(token_id) if AA_SERVER
+ end
+
end
diff --git a/lib/opentox-ruby.rb b/lib/opentox-ruby.rb
index 9f9ff26..c0bff95 100644
--- a/lib/opentox-ruby.rb
+++ b/lib/opentox-ruby.rb
@@ -8,6 +8,6 @@ rescue LoadError
puts "Please install Openbabel with 'rake openbabel:install' in the compound component"
end
-['opentox', 'compound','dataset', 'parser','serializer', 'algorithm','model','task','validation','feature', 'rest_client_wrapper'].each do |lib|
+['opentox', 'compound','dataset', 'parser','serializer', 'algorithm','model','task','validation','feature', 'rest_client_wrapper', 'authorization', 'policy', 'helper'].each do |lib|
require lib
end
diff --git a/lib/policy.rb b/lib/policy.rb
new file mode 100644
index 0000000..0ef8298
--- /dev/null
+++ b/lib/policy.rb
@@ -0,0 +1,242 @@
+module OpenTox
+ require "rexml/document"
+
+ #Module for policy-processing
+ # @see also http://www.opentox.org/dev/apis/api-1.2/AA for opentox API specs
+ # Class Policies corresponds to <policies> container of an xml-policy-fle
+ class Policies
+
+ attr_accessor :name, :policies
+
+ def initialize()
+ @policies = {}
+ end
+
+ #create new policy instance with name
+ # @param [String]name of the policy
+ def new_policy(name)
+ @policies[name] = Policy.new(name)
+ end
+
+ #drop a specific policy in a policies instance
+ # @param [String]name of the policy
+ # @return [Boolean]
+ def drop_policy(name)
+ return true if @policies.delete(name)
+ end
+
+ #drop all policies in a policies instance
+ def drop_policies
+ @policies.each do |name, policy|
+ drop_policy(name)
+ end
+ return true
+ end
+
+ #loads a default policy template in policies instance
+ def load_default_policy(user, uri, group="member")
+ template = case user
+ when "guest", "anonymous" then "default_guest_policy"
+ else "default_policy"
+ end
+ xml = File.read(File.join(File.dirname(__FILE__), "templates/#{template}.xml"))
+ self.load_xml(xml)
+ datestring = Time.now.strftime("%Y-%m-%d-%H-%M-%S-x") + rand(1000).to_s
+
+ @policies["policy_user"].name = "policy_user_#{user}_#{datestring}"
+ @policies["policy_user"].rules["rule_user"].uri = uri
+ @policies["policy_user"].rules["rule_user"].name = "rule_user_#{user}_#{datestring}"
+ @policies["policy_user"].subjects["subject_user"].name = "subject_user_#{user}_#{datestring}"
+ @policies["policy_user"].subjects["subject_user"].value = "uid=#{user},ou=people,dc=opentox,dc=org"
+ @policies["policy_user"].subject_group = "subjects_user_#{user}_#{datestring}"
+
+ @policies["policy_group"].name = "policy_group_#{group}_#{datestring}"
+ @policies["policy_group"].rules["rule_group"].uri = uri
+ @policies["policy_group"].rules["rule_group"].name = "rule_group_#{group}_#{datestring}"
+ @policies["policy_group"].subjects["subject_group"].name = "subject_group_#{group}_#{datestring}"
+ @policies["policy_group"].subjects["subject_group"].value = "cn=#{group},ou=groups,dc=opentox,dc=org"
+ @policies["policy_group"].subject_group = "subjects_#{group}_#{datestring}"
+ return true
+ end
+
+ #loads a xml template
+ def load_xml(xml)
+ rexml = REXML::Document.new(xml)
+ rexml.elements.each("Policies/Policy") do |pol| #Policies
+ policy_name = pol.attributes["name"]
+ new_policy(policy_name)
+ #@policies[policy_name] = Policy.new(policy_name)
+ rexml.elements.each("Policies/Policy[@name='#{policy_name}']/Rule") do |r| #Rules
+ rule_name = r.attributes["name"]
+ uri = rexml.elements["Policies/Policy[@name='#{policy_name}']/Rule[@name='#{rule_name}']/ResourceName"].attributes["name"]
+ @policies[policy_name].rules[rule_name] = @policies[policy_name].new_rule(rule_name, uri)
+ rexml.elements.each("Policies/Policy[@name='#{policy_name}']/Rule[@name='#{rule_name}']/AttributeValuePair") do |attribute_pairs|
+ action=nil; value=nil;
+ attribute_pairs.each_element do |elem|
+ action = elem.attributes["name"] if elem.attributes["name"]
+ value = elem.text if elem.text
+ end
+ if action and value
+ case action
+ when "GET"
+ @policies[policy_name].rules[rule_name].get = value
+ when "POST"
+ @policies[policy_name].rules[rule_name].post = value
+ when "PUT"
+ @policies[policy_name].rules[rule_name].put = value
+ when "DELETE"
+ @policies[policy_name].rules[rule_name].delete = value
+ end
+ end
+ end
+ end
+ rexml.elements.each("Policies/Policy[@name='#{policy_name}']/Subjects") do |subjects| #Subjects
+ @policies[policy_name].subject_group = subjects.attributes["name"]
+ rexml.elements.each("Policies/Policy[@name='#{policy_name}']/Subjects[@name='#{@policies[policy_name].subject_group}']/Subject") do |s| #Subject
+ subject_name = s.attributes["name"]
+ subject_type = s.attributes["type"]
+ subject_value = rexml.elements["Policies/Policy[@name='#{policy_name}']/Subjects[@name='#{@policies[policy_name].subject_group}']/Subject[@name='#{subject_name}']/AttributeValuePair/Value"].text
+ @policies[policy_name].new_subject(subject_name, subject_type, subject_value) if subject_name and subject_type and subject_value
+ end
+ end
+ end
+ end
+
+ #generates xml from policies instance
+ def to_xml
+ doc = REXML::Document.new()
+ doc << REXML::DocType.new("Policies", "PUBLIC \"-//Sun Java System Access Manager7.1 2006Q3\n Admin CLI DTD//EN\" \"jar://com/sun/identity/policy/policyAdmin.dtd\"")
+ doc.add_element(REXML::Element.new("Policies"))
+
+ @policies.each do |name, pol|
+ policy = REXML::Element.new("Policy")
+ policy.attributes["name"] = pol.name
+ policy.attributes["referralPolicy"] = false
+ policy.attributes["active"] = true
+ @policies[name].rules.each do |r,rl|
+ rule = @policies[name].rules[r]
+ out_rule = REXML::Element.new("Rule")
+ out_rule.attributes["name"] = rule.name
+ servicename = REXML::Element.new("ServiceName")
+ servicename.attributes["name"]="iPlanetAMWebAgentService"
+ out_rule.add_element(servicename)
+ rescourcename = REXML::Element.new("ResourceName")
+ rescourcename.attributes["name"] = rule.uri
+ out_rule.add_element(rescourcename)
+
+ ["get","post","delete","put"].each do |act|
+ if rule.method(act).call
+ attribute = REXML::Element.new("Attribute")
+ attribute.attributes["name"] = act.upcase
+ attributevaluepair = REXML::Element.new("AttributeValuePair")
+ attributevaluepair.add_element(attribute)
+ attributevalue = REXML::Element.new("Value")
+ attributevaluepair.add_element(attributevalue)
+ attributevalue.add_text REXML::Text.new(rule.method(act).call)
+ out_rule.add_element(attributevaluepair)
+
+ end
+ end
+ policy.add_element(out_rule)
+ end
+
+ subjects = REXML::Element.new("Subjects")
+ subjects.attributes["name"] = pol.subject_group
+ subjects.attributes["description"] = ""
+ @policies[name].subjects.each do |subj, subjs|
+ subject = REXML::Element.new("Subject")
+ subject.attributes["name"] = pol.subjects[subj].name
+ subject.attributes["type"] = pol.subjects[subj].type
+ subject.attributes["includeType"] = "inclusive"
+ attributevaluepair = REXML::Element.new("AttributeValuePair")
+ attribute = REXML::Element.new("Attribute")
+ attribute.attributes["name"] = "Values"
+ attributevaluepair.add_element(attribute)
+ attributevalue = REXML::Element.new("Value")
+ attributevalue.add_text REXML::Text.new(pol.subjects[subj].value)
+ attributevaluepair.add_element(attributevalue)
+ subject.add_element(attributevaluepair)
+ subjects.add_element(subject)
+ end
+ policy.add_element(subjects)
+ doc.root.add_element(policy)
+ end
+ out = ""
+ doc.write(out, 2)
+ return out
+ end
+
+ end
+
+ #single policy in a policies instance
+ class Policy
+
+ attr_accessor :name, :rules, :subject_group, :subjects
+
+ def initialize(name)
+ @name = name
+ @rules = {}
+ @subject_group = ""
+ @subjects = {}
+ end
+
+ #create a new rule instance for the policy
+ def new_rule(name, uri)
+ @rules[name] = Rule.new(name, uri)
+ end
+
+ #create a new subject instance for the policy
+ def new_subject(name, type, value)
+ @subjects[name] = Subject.new(name, type, value)
+ end
+
+ #rule inside a policy
+ class Rule
+
+ attr_accessor :name, :uri, :get, :post, :put, :delete
+
+ def initialize(name, uri)
+ @name = name
+ @uri = uri
+ end
+
+ def rename(new, old)
+ self[new] = self.delete(old)
+ self[new].name = new
+ end
+
+ def get=(value)
+ @get = check_value(value, @get)
+ end
+
+ def post=(value)
+ @post = check_value(value, @post)
+ end
+
+ def delete=(value)
+ @delete = check_value(value, @delete)
+ end
+
+ def put=(value)
+ @put = check_value(value, @put)
+ end
+
+ private
+ #checks if value is allow or deny. returns old value if not valid.
+ def check_value(new_value, old_value)
+ return (new_value=="allow" || new_value=="deny" || new_value==nil) ? new_value : old_value
+ end
+ end
+
+ class Subject
+
+ attr_accessor :name, :type, :value
+
+ def initialize(name, type, value)
+ @name = name
+ @type = type
+ @value = value
+ end
+ end
+ end
+end \ No newline at end of file