YaST/Web/Development/Tutorial

From openSUSE

(Difference between revisions)
Revision as of 11:54, 6 May 2009
Bgeuken (Talk | contribs)
Defining a new class
� Previous diff
Revision as of 13:44, 22 May 2009
Mvidner (Talk | contribs)
typo fixes
Next diff →
Line 14: Line 14:
All modules of WebYast are stored in the plug-ins dirctory, so open a terminal and All modules of WebYast are stored in the plug-ins dirctory, so open a terminal and
-<pre>+ 
-rest-service/webservice/plugins+ cd rest-service/plugins
-</pre>+ 
At least all modules should have the directories ''app config lib package tasks'' and ''test'', create them. Another way would be to copy the files of another directory like e.g. systemtime. At least all modules should have the directories ''app config lib package tasks'' and ''test'', create them. Another way would be to copy the files of another directory like e.g. systemtime.
Line 26: Line 26:
==The model== ==The model==
-Now change to app/model and create the file security.rb. This is were you can define the security class with all it's attributes and methods. +Now change to app/model and create the file security.rb. This is where you can define the security class with all its attributes and methods.
Like in every module security class should have error_id and error_string variables to assign different types of errors. We also need 3 variables containing boolean values which are running_on_startup, firewall_running and ssh_running. Like in every module security class should have error_id and error_string variables to assign different types of errors. We also need 3 variables containing boolean values which are running_on_startup, firewall_running and ssh_running.
Line 47: Line 47:
xml.security do xml.security do
xml.tag!(:ssh_running, @ssh_running) xml.tag!(:ssh_running, @ssh_running)
- xml.tag!(:firewall_runnning, @firewall_running)+ xml.tag!(:firewall_running, @firewall_running)
xml.tag!(:running_on_startup, @running_on_startup) xml.tag!(:running_on_startup, @running_on_startup)
xml.tag!(:error_id, @error_id, {:type => "integer"}) xml.tag!(:error_id, @error_id, {:type => "integer"})
Line 62: Line 62:
===Defining a new class=== ===Defining a new class===
Change directory to the controller and create the file security_controller.rb Change directory to the controller and create the file security_controller.rb
-cd ../controller+ cd ../controller
-vi security_controller.rb+ vi security_controller.rb
ApplicationController is part of the rest-service and provides some methods which we will need. So define the class SecurityController and derivate it from the ApplicationController. ApplicationController is part of the rest-service and provides some methods which we will need. So define the class SecurityController and derivate it from the ApplicationController.

Revision as of 13:44, 22 May 2009

Contents

Developing the module

Please notice that this site is momentarily under construction

This tutorial will show you how to create a module for the WebYast. The module will enable a user to start or stop the ssh-daemon and the firewall and additionally decide if the firewall shall be started after reboot.

While the rest-service take care for the functionality, the web-client gives the user a fine gui for the browser.

We will start with the rest-service.

Rest-Service:

After checking out the source from the git repo, try to run both, the rest-service and the web-client.

All modules of WebYast are stored in the plug-ins dirctory, so open a terminal and

 cd rest-service/plugins

At least all modules should have the directories app config lib package tasks and test, create them. Another way would be to copy the files of another directory like e.g. systemtime.

It can be helpful to open the development log in a second terminal while developing on WebYast.

tail -fn 100 log/development.log

The model

Now change to app/model and create the file security.rb. This is where you can define the security class with all its attributes and methods. Like in every module security class should have error_id and error_string variables to assign different types of errors. We also need 3 variables containing boolean values which are running_on_startup, firewall_running and ssh_running.

Ruby's attr_accessor defines how to handle read/write access on our variables.

class Security
  attr_accessor :error_id,
                :error_string,
                :running_on_startup,
                :firewall_running,
                :ssh_running

To respond on requests in xml or json format, we need to define some methods to do so.

  def to_xml(options = {})
    xml = options[:builder] ||= Builder::XmlMarkup.new(options)
    xml.instruct! unless options[:skip_instruct]

    xml.security do
      xml.tag!(:ssh_running, @ssh_running)
      xml.tag!(:firewall_running, @firewall_running)
      xml.tag!(:running_on_startup, @running_on_startup)
      xml.tag!(:error_id, @error_id, {:type => "integer"})
      xml.tag!(:error_string, @error_string)
    end
  end

The method to_xml converts the attributes of the security class an sends them in xml format.

The controller

The controller connects the model on the service-side with representation on the web-client side. The web-client sends a GET-Request the rest-service and expects the current status as response. This can be either in xml or in json format like we defined in our model before.

Defining a new class

Change directory to the controller and create the file security_controller.rb

cd ../controller
vi security_controller.rb

ApplicationController is part of the rest-service and provides some methods which we will need. So define the class SecurityController and derivate it from the ApplicationController. Require the scr library as well, it will enable you to access/use the yast api. The scr library is part of the rest-service and lies inside the lib directory.

include ApplicationHelper

class SecurityController < ApplicationController

  require "scr"

The before_filter will execute login_required when any action where called. It checks if the user is logged in and redirects him, if not.

  before_filter :login_required

Now let's define the show method which has to request GET requests and collects the data.

  def show
    @security = System::Security.new

    if permission_check("org.opensuse.yast.webservice.read-security")
      @security.firewall_running = get_firewall
      @security.running_on_startup = get_firewall_after_startup
      @security.ssh_running = get_ssh

Be aware that there isn't a Policy for the security module yet. Anyways permission_check verifies if a user has the necessary permission. How to create new policies is described here. The current status of firewall and ssh were collected by the private methods get_firewall, get_firewall_after_startup and get_ssh.

In case the check fails error_string will give a hint what was wrong.

      else #no permissions
        security.error_id = 1
        security.error_string = "no permission"
      end

Responding to the client

For every format in which we want to reply we need to define how. Now use the to_xml method defined in the model. It then returns the xml format which will be rendered for the response. Notice that also format.html render in xml format.

    respond_to do |format|
      format.html do
        render :xml => @security.to_xml( :root => "security",
          :dasherize => false ), :location => "none" #return xml value only
      end
      format.xml do
        render :xml => @security.to_xml( :root => "security",
          :dasherize => false ), :location => "none"
      end
      format.json do
        render :json => @security.to_json , :location => "none"
      end
    end
  end
end

Collecting the Status

We still need to define the get methods to collect the data. On the command-line someone would probably use rcsshd status and rcSuSEfirewall2 status to get the first two values. Why not using ruby's IO class to do exactly the same?

  private
  def firewall?
    IO.popen("rcSuSEfirewall2 status", 'r+') do |pipe|
      break if pipe.eof?
      if pipe.read.to_str.include? "running"
        return true
      else#if retval.include? "unused"
        return false
      end
    end
  end

When the firewall is running the command sends back a string containing running, if not it contains unused. Check this to get the right state and store either true or false in a variable as return value. Mention that rubx on rails always expect the last value as return value. So no need to write anything like return ret.

get_ssh works nearly the same.

  def ssh?
    IO.popen("rcsshd status", 'r+') do |pipe|
      break if pipe.eof?
      if pipe.read.to_str.include? "running"
        return true
      else #if retval.include? "unused"
        return false
      end
    end

Now the only thing missing is the firewall_after_startup? method. On the command-line we could use /sbin/yast2 firewall startup show to get the information, but this won't work with popen. It will only block the systembus and maybe create some zombies. So, let's use the scr-library of the rest-service. Again, the rest works nearly the same.

  def firewall_after_startup?
    cmd = Scr.execute(["/sbin/yast2", "firewall", "startup", "show"])
    lines = cmd[:stderr].split "\n"
    lines.each do |l|
      if l.length > 0
        if l.include? "enabled"
          return true
        elsif l.include? "manual"
          return false
        end
      end
    end
    #ret
  end

Defining routes

Adding some lines in config/routes.rb enables the rest-service to route the requests of the web-client to the right action:

ActionController::Routing::Routes.draw do |map|

  map.resources :security, :controller => 'security'
  map.connect "/security/:id", :controller => 'security', :action => 'singlevalue'
  map.connect "/security/:id.xml", :controller => 'security', :action => 'singlevalue', :format =>'xml'
  map.connect "/security/:id.html", :controller => 'security', :action => 'singlevalue', :format =>'html'
  map.connect "/security/:id.json", :controller => 'security', :action => 'singlevalue', :format =>'json'

  map.namespace :security do |security|
      security.resource :dummy
  end

  map.resources :security do |security|
     security.resources :commands
  end

end

When a application receives a request, the routing will determine which controller and action to run.

Have a look in the browser

At this point, the rest-service can be watched inside a browser. Just make sure that you already defined and granted the security policies.

start the rest-service
script/server webrick -p 3001

Now open a browser and go to http://localhost:3001/. You will see the different modules, but no the security module. That is because we didn't added it to the yast_controller yet. Open webservice/app/controller/yast_controller.rb and add the link for the security module.

     link = Links.new
     link.path = "security"
     link.description = "Setting firewall and ssh-daemon"
     if permission_check("org.opensuse.yast.webservice.read-security")
        link.read_permission = true
     else
        link.read_permission = false
     end
     if permission_check("org.opensuse.yast.webservice.write-security")
        link.write_permission = true
     else
        link.write_permission = false
     end
     link.execute_permission = false
     link.delete_permission = false
     link.new_permission = false
     link.install_permission = false
     @links << link

Now reload the browser page and try again. The before_filter in the controller redirects you to a login screen and you can put in your credentials. Normally this will be root. You can see the xml formated security instance.

Web-Client

Like on the service all modules are stored in the plugins dirctory. Create a security directory in the plugins directory and at least the subdirectories.

mkdir web-client/plugins/security
cd web-client/plugins/security
mkdir app package tasks test
cd app
mkdir app model view controller

The model

class Security < MyActiveResource
    self.collection_name = "security"
    self.element_name = "security"
end

The controller

Change to th controller dir

cd plugins/app/controller/
vi security_controller.rb 

and define the SecurityController class which derivates from ApplicationController. Create a before_filter as well.

class SecurityController < ApplicationController
  before_filter :login_required
  def index
    set_permissions(controller_name)
    @security = Security.find(:one, :from => '/security.xml')

set_permission checks the users permissions granted from the rest-service and sets them on the web-client side. Then we request the security.xml file and store their values in @security. The only thing left is to evaluate the values and prepare the variables for the view. This happens down below, where we store either "checked" or an empty string into them. In the view we later get a checked or an empty box to show for the web-frontend.

    if @security.running_on_startup == true
      @running_on_startup = "checked"
    else
      @running_on_startup = ""
    end
   if @security.firewall_running == true
      @firewall_running = "checked"
    else
      @firewall_running = ""
    end
    if @security.ssh_running == true
      @ssh_running = "checked"
    else
      @ssh_running = ""
    end
  end

The view

mkdir view/security
vi view/security/index.rhtml

On the web-client side the view consists of 3 checkboxes, which are showing the current status of firewall, ssh and the automatic enabled firewall. To change the values one have to check or uncheck the boxes and transmit them with the Submit Settings button.

<h1><%=_("Security")%></h1>
<br>

<% form_tag '/security/commit' do %>
  <div class="table">
    <img src="/images/bg-th-left.gif" width="8" height="7" alt="" class="left" />
    <img src="/images/bg-th-right.gif" width="7" height="7" alt="" class="right" />
    <table class="listing form" cellpadding="0" cellspacing="0">
       <tr>
          <th class="full" colspan="2"><%=_("Settings")%></th>
       </tr>
       <tr class>
          <td class="first"><strong><%=_("Enable Firewall Automatic Starting")%></strong></td>
          <td class="last"><input type="checkbox" name="running_on_startup" value="true" 
                                  <%= @running_on_startup %> <%= "disabled=\"disabled\"" 
                                              if @write_permission=="disabled" %> ></td>
       </tr>
      <tr class="bg">
          <td class="first"><strong><%=_("Current Firewall Status")%> </strong></td><td class="last">
                 <input type="checkbox" name="firewall_running" value="true" <%= @firewall_running %>
                               <%= "disabled=\"disabled\"" if @write_permission=="disabled" %> ></td>
       </tr>
       <tr class>
          <td class="first"><strong><%=_("Current SSH Status")%></strong></td>
          <td class="last"><input type="checkbox" name="ssh_running" value="true" <%= @ssh_running %> 
                                <%= "disabled=\"disabled\"" if @write_permission=="disabled" %> ></td>
       </tr>
    </table>
    <p><%= submit_tag _("Submit Settings"), :onclick=>"Element.show('progress')", :disabled=>@write_permission -%></p>
  </div>
<% end %>

The views/security/index.html.erb is nearly plain html. Only the variables for the checkboxes and permissions are embedded with ruby-code. By calling set_permissions in the controller, we have set @security-permissions variable. This line decides if the user is able to edit the checkbox or not.

<%= "disabled=\"disabled\"" if @write_permission=="disabled" %>

A look in the browser again

Now you can start the web-client and open a browser.

script/server

Go to http://localhost:3001, login and choose the security module. You can see the 3 options and can change them. After clicking the submit button, none of these changes will be sent. Actually we already added a commit action to the format tag,

 <% form_tag '/security/commit' do %>

but haven't defined any commit action in the controller. Let's do this now.

Adding new action to the controller

Define a new method and create a new empty Security instance. The params hash contains the POST params of the view. Make a check for every variable and store true or false into the security instance.

  def commit
    s = Security.new()
    if params[:running_on_startup] == "true"
      s.running_on_startup = true
    else
      s.running_on_startup = false
    end
    if params[:firewall_running] == "true"
      s.firewall_running = true
    else
      s.firewall_running = false
    end
    if params[:ssh_running] == "true"
      s.ssh_running = true
    else
      s.ssh_running = false
    end

The save method sends a POST with the security instance to /security. We still need to define what to respond from the rest-service side. The error_id and error_string can give valid informations about any error which might occur. So if anything went wrong, a noticeable error message made of the error_string will be shown. Else the user will get the notification that the settings have been written. In both cases the user got redirected to the view of index.html.erb

    s.save
    if s.error_id != 0
      flash[:error] = s.error_string
      redirect_to :action => :index
    else
      flash[:notice] = _('Settings have been written.')
      redirect_to :action => :index
    end
  end

Rest-service Part 2

What's still missing is an action to respond to the POST request, when a user commits changes from the web-client. Therefor we define a create action inside the security_controller.

  def create
    respond_to do |format|
      @security = System::Security.new()
      if permission_check("org.opensuse.yast.webservice.write-security")

This time the user needs to be checked for his write permissions. If it succeeds the set_firewall, set_ssh and set_firewall_after_startup sets the new values for the class attributes.

        if params[:security] != nil
          @security.firewall = set_firewall params[:security][:firewall_running]
          @security.ssh =  set_ssh params[:security][:ssh_running]
          @security.running_on_startup = set_running_on_startup params[:security][:running_on_startup]
          logger.debug "UPDATED: #{@security.inspect}"
        else
          @security.error_id = 2
          @security.error_string = "format or internal error"
        end
      else #no permission
        @security.error_id = 1
        @security.error_string = "no permission"
      end

Finally create sends the new data of security class back to the client.

      format.html do
        render :xml => @security.to_xml( :root => "security",
          :dasherize => false ), :location => "none" #return xml value only
      end
      format.xml do
        render :xml => @security.to_xml( :root => "security",
          :dasherize => false ), :location => "none"
      end
      format.json do
        render :json => @security.to_json , :location => "none"
      end
    end
  end

The methods set_firewall, set_firewall_after_startup and set_ssh are working nearly the same way. After evaluating the parameter, the firewall/sshd will set. The return value shows if the operation succeeded or not.

  def firewall(param)
    if param == true   # start firewall
      action = "restart"
    else               # stop firewall
      action = "stop"
    end
    IO.popen("rcSuSEfirewall2 #{action}", 'r+') do |pipe|
      break if pipe.eof?
      retval = pipe.read.to_str
      if retval.include? "done" #success
        return true
      end
    end
    return false
  end
  def ssh(param)
    if param == true   # start sshd
      action = "restart"
    else
      action = "stop"  # stop sshd
    end
    IO.popen("rcsshd #{action}", 'r+') do |pipe|
      break if pipe.eof?
      retval = pipe.read.to_str
      if retval.include? "done" #success
        return true
      end
    end
    return false
  end
  def set_firewall_after_startup(param)
    if param == true    # enable
      action = "atboot"
      verify = "Enabling"
    else                # disable
      action = "manual"
      verify = "Removing"
    end
    cmd = Scr.execute(["/sbin/yast2", "firewall", "startup", "#{action}"])
    lines = cmd[:stderr].split "\n"
    lines.each do |l|
      if l.length > 0
        if l.include? "#{verify}"
          return true
        end
      end
    end
    return false
  end

In the end you have a full functional module to set up firewall and ssh-daemon via the web browser.

PolkitPolicies

Defining PolkitPolicies

All policies of WebYast are described in /usr/share/PolicyKit/policy/org.opensuse.yast.webservice.policy. To create a new policy like org.opensuse.yast.webservice.read-security you can add something to the file like

<action id="org.opensuse.yast.webservice.read-security">
  <description>Reading firewall settings</description>
  <message>Authentication is required to read firewall settings</message>
  <defaults>
    <allow_inactive>no</allow_inactive>
    <allow_active>no</allow_active>
  </defaults>
</action>

and to create org.opensuse.yast.webservice.write-security

<action id="org.opensuse.yast.webservice.write-security">
  <description>Reading firewall settings</description>
  <message>Authentication is required to read firewall settings</message>
  <defaults>
    <allow_inactive>no</allow_inactive>
    <allow_active>no</allow_active>
  </defaults>
</action>

Granting PolkitPolicies

Now we can grant the new policies to a user, eg. root

polkit-auth --user root –grant org.opensuse.yast.webservice.write-security
polkit-auth --user root –grant org.opensuse.yast.webservice.read-security

After defining the policies it is possible to watch the actual status in the browser: http://localhost:3000/security. But there isn't any funtionality, new settings won't be transmitted. This will be enabled by adding the create method to the security_controller.rb of the rest-service.