YaST/Web/Development/Tutorial

From openSUSE

(Difference between revisions)
Revision as of 13:49, 16 June 2009
Bgeuken (Talk | contribs)
The view
� Previous diff
Revision as of 07:52, 1 July 2009
Cthiel1 (Talk | contribs)
fix typo
Next diff →
Line 63: Line 63:
<pre> <pre>
git clone git://git.opensuse.org/projects/yast/web-client.git git clone git://git.opensuse.org/projects/yast/web-client.git
-cd web-cient+cd web-client
make make
</pre> </pre>

Revision as of 07:52, 1 July 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 ssh-daemon and firewall and additionally decide if the firewall shall be started during the boot process.

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

Before we can start with the program, we will define the permissions for the new module.


PolkitPolicies

Defining PolkitPolicies

Every Yast Module provides it's own policy file, which defines the actions a user can be granted to. They were stored in /usr/share/PolicyKit/policy/ and they usually have the form org.opensuse.yast.system.module_name.policy. For the security module we will need something like write and read access, so create the org.opensuse.yast.system.security.policy file and add these lines to it:

	 
<?xml version="1.0" encoding="UTF-8"?>	 
<!DOCTYPE policyconfig PUBLIC	 
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"	 
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">	 
<policyconfig>	 
	 
<vendor>YaST Webservice Project</vendor>	 
<vendor_url>http://en.opensuse.org/YAST</vendor_url>	 
	 
<!--	 
Rights for security	 
-->	 
	 
<action id="org.opensuse.yast.system.security.read">	 
<description>Reading security settings</description>	 
<message>Authentication is required to read security settings</message>	 
<defaults>	 
<allow_inactive>no</allow_inactive>	 
<allow_active>no</allow_active>	 
</defaults>	 
</action>	 
<action id="org.opensuse.yast.system.security.write">	 
<description>Writing security settings</description>	 
<message>Authentication is required to write security settings</message>	 
<defaults>	 
<allow_inactive>no</allow_inactive>	 
<allow_active>no</allow_active>	 
</defaults>	 
</action>	 
	 
</policyconfig>	 
	 

Granting PolkitPolicies

If you want to watch your module during development, you need to give some permissions 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	 

Rest-Service:

Preparing

To start developing you will need the sources, so check out the git repos and install the required packages.

git clone git://git.opensuse.org/projects/yast/web-client.git
cd web-client
make

It is very likely that the make command doesn't succeeded, because of some package dependencies. Have a look into the spec file in webclient/package/yast2-webclient.spec and install the required packages. You can find the packages either at the [service] or at [[1]]. Then try again make.

Now as you have successfully installed the web-client, you can go on with the rest-service.


All modules of WebYast are stored in the plug-ins dirctory, so open a terminal and create a new directory for your modul

  mkdir rest-service/plugins/security
  cd rest-service/plugins/security

At least your new module should have the directories app config lib package tasks and test. Go on and create them.

Now change directory to security/app and create directories for the model, view and controller.

mkdir models view controller

During development it can be helpful to open the development log in a second terminal.

tail -fn 100 log/development.log

The model

In the model you will define the security class with all it's attributes and methods. We will need 3 variables which were firewall_on_startup, firewall and ssh. They all containing boolean values to show the current status. We will also need some methods to grab these informations and provide them when requested.

Change directory to app/models and create the file security.rb.

The access to the variables can be defined with Ruby's attr_reader. We will only need read access here and define methods for writing and reading to the system later.

class Security
  require "scr"

  attr_reader  :firewall,
               :firewall_on_startup,
               :ssh

Notice that we have included scr which is stored in the lib directory of the main program, rest-service/webservice/lib/scr.rb.

As we don't need a database we don't inherit Security from ActiveRecord::Base. But we need to define our own to_xml and to_json methods:

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

    xml.security do
      xml.tag!(:ssh, @ssh, {:type => "boolean"})
      xml.tag!(:firewall, @firewall, {:type => "boolean"})
      xml.tag!(:firewall_on_startup, @firewall_on_startup, {:type => "boolean"})
    end
  end

  def to_json(options = {})
    hash = Hash.from_xml(to_xml())
    return hash.to_json
  end

These methods convert the attributes of the security class into xml or json format and provide them as return value.

At that point Security class has some variables. Someone can read them or ask for a nice xml or json format, but there isn't any functionality. Now let's provide some:

  def initialize
    @scr = Scr.instance
    @firewall = firewall?
    @firewall_on_startup = firewall_on_startup?
    @ssh = ssh?
  end

With Security.new() one can get an instance of Security class. It will generate a new instance of scr and asks for the current system status.

  def update
    @firewall = firewall?
    @firewall_on_startup = firewall_on_startup?
    @ssh = ssh?
  end

Updates the status without instantiate a new scr object.

Now we just need to define the missing methods which are more or less the get methods for the attributes.

Don't use IO methods at this point. It will bring you into trouble. As WebYast will run as a service it will have it's own user yastws. This user won't have root permissions, but get access to the system via the scr lib and the PolicyKit permissions we have defined before.

  def firewall?
    cmd = @scr.execute(["/sbin/rcSuSEfirewall2", "status"])

Here we hand over the /sbin/rcSuSEfirewall2 command and all parameters in separated strings. The execute function will pass it via dbus to the system. The return value will be written into cmd. Whether the command responded through stdout or stderr, the output can be accessed with cmd[:stdout] or cmd[:stderr].

    lines = cmd[:stdout].split "\n"
    lines.each do |l|
      if l.length > 0
        if l.include? "running"
          return true
        elsif l.include? "unused"
          return false
        end
      end
    end
    return nil
  end

The firewall? methods returns true if the firwall is enabled, false if it is disabled and nil if any error occurs.

  def ssh?
    cmd = @scr.execute(["/usr/sbin/rcsshd", "status"])
    lines = cmd[:stdout].split "\n"
    lines.each do |l|
      if l.length > 0
        if l.include? "running"
          return true
        elsif l.include? "unused"
          return false
        end
      end
    end
    return nil
  end

The ssh method does nearly the same, while firewall_on_startup? uses the Yast interface.

  def firewall_on_startup?
    cmd = @scr.execute(["/sbin/yast2", "firewall", "startup", "show"])
    lines = cmd[:stderr].split "\n"

The response of yast2 firewall were also provided via stderr.

    lines.each do |l|
      if l.length > 0
        if l.include? "enabled"
          return true
        elsif l.include? "manual"
          return false
        end
      end
    end
    return nil
  end

Now, as we can read the system state of firewall and ssh we can go on with the controller.


The controller

The controller connects the model on the service-side with the 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
include ApplicationHelper

class SecuritiesController < 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.

  before_filter :login_required

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

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

  def show
    unless permission_check("org.opensuse.yast.system.security.read")
      render ErrorResult.error(403, 1, "no permission") and return
    else
      @security = Security.new
    end
  end

First permission_check checks the user for his rights. If he isn't allowed to read security informations, the show action rise an error 403 and stops. Else it creates a new Security object, which means the initialize() method of the model grabs the current status of firewall and ssh.

The show action responds the Security object to the web-client, which has to handle the display inside the browser. This will happen here. But first you have to define routes for your new module.

Defining Routes

The routes for every module is defined in config/resources/security.yml inside the plugin/security directory.

interface: org.opensuse.yast.system.security
controller: securities
singular: true

You can watch your new module at http://localhost:3000 with your browser. The link name is the value of interface. You can also go directly to http://localhost:3000/security or type http://localhost:3000/security.xml to get a xml tree.

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 view controller in the app dirctory.

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

The controller

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

cd plugins/app/controller/
vi security_controller.rb

Now define the SecurityController class and derivate it from ApplicationController and include yast/service_resource. Create a before_filter as well.

require 'yast/service_resource'

class SecurityController < ApplicationController
  before_filter :login_required, :prepare

This will start the login_required action like on the controller of the rest-service, but also starts the prepare action before executing any other action inside the security controller.

  private

  def prepare
    @client = YaST::ServiceResource.proxy_for('org.opensuse.yast.system.security')
    @permissions = @client.permissions
    set_permissions(controller_name)
  end

The prepare action opens a proxy for the action and get the permission settings for the module. Then these values will be written via the set_permissions() function.

  def create
    @security = @client.find(:one, :from => '/security.xml')

Request the security values from the rest-service in xml format and ...

    @firewall_on_startup = "checked" if @security.firewall_on_startup
    @firewall = "checked" if @security.firewall
    @ssh = "checked" if @security.ssh
  end

... prepare the @firewall_on_startup, @firewall, @ssh for the view. Depending on the value of the variables we store "checked" for true or an empty string for false.

The view

Create a new directory called security inside the view and write a view for the create action.

mkdir view/security
vi view/securities/create.erb

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 can check or uncheck the boxes and transmit them with the Submit Settings button.

<h2><%=_("Security")%></h2>

<% form_tag '/security/update' do %>
<span>
  <label>
    <input type="checkbox" name="firewall_on_startup" value="true" <%= @firewall_on_startup %> @write_permission />
    <span><%=_("Enable Firewall Automatic Starting")%></span>
  </label>

  <label>
    <input type="checkbox" name="firewall" value="true" <%= @firewall %> @write_permission />
    <span><%=_("Firewall enabled")%></span>
  </label>

 <label>
    <input type="checkbox" name="ssh" value="true" <%= @ssh %> @write_permission />
    <span><%=_("SSH login enabled")%></span>
  </label>

  <span>
    <hbox>
      <%= submit_tag _("Save"), :onclick=>"Element.show('progress')", :disabled=>@write_permission.nil?, :class => 'button' -%>
      <a href="/" class="button"><%=_("Back")%></a>
    </hbox>
  </span>
</span>
<% end %>

The views/security/create.erb is nearly plain html. Only the variables for the checkboxes and permissions are embedded within ruby-code. By calling set_permissions in the controller, we have set @security-permissions variable. The @write_permission contains either "disabled" or "" and decides if the user is able to edit the checkbox or not. It has beend writen through the set_permission function in the prepare action of the controller.

<input type="checkbox" name="firewall" value="true" <%= @firewall %> '@write_permission' />


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

script/server -p 3001

Go to http://localhost:3001, login and choose the security module. Make sure that the rest-service is still running and set the url and port-number to the web-client settings. After doing that you can see the 3 options for ssh and firewall and change them. In case there you got an error 403, you should make sure that you have granted the policies to the right user.

After clicking the submit button, none of these changes will be sent. Actually we already added an update action to the format tag of the view,

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

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

Adding new action to the controller

By calling @client.find() we request a new empty Security instance from the rest-service. The params hash contains the POST parameter of the view. Then the update action checks the params for the string "true" and stores the return value (true or false) into the security instance.

  def update
    s = @client.find()
    s.firewall_on_startup = params[:firewall_on_startup].eql?("true")
    s.firewall = params[:firewall].eql?("true")
    s.ssh = params[:ssh].eql?("true")

The save method then sends a POST with the security instance to the security module of the rest-service. If it responds with an error, the web-client flashes an error message.

    response = true
    begin
      response = s.save # send to rest-service
      rescue ActiveResource::ClientError => e
        flash[:error] = YaST::ServiceResource.error(e)
        response = false
    end
    flash[:notice] = _("Settings have been written.") if response

    redirect_to root_url
  end

At the end the user got redirected to the root url, which is localhost:3001/webservice.

Update View

As we use the update action, we need to provide an update view. Create the update.erb in the view directory.

Basically it is the same file like create.erb. You just need to copy it into update.erb.

cp view/secvurity/create.erb view/security/update.erb

We still need to define what to respond from the rest-service side. So change to the rest-service and open the security_controller.rb file.

Rest-service Part 2

On the rest-service side the controller will receive a POST request with some parameters from the web-client. These parameters contains the new values of ssh and firewall and needed to be submitted to the system where the rest-service resides. Therefor we need some new methods inside our Security class of the model.

The Model

The write method gives a small interface to the model, which we can use later in the controller. It expects three boolean values and hands them over to firewall(), firewall_on_startup() and ssh().

  #firewall,firewall_on_startup,ssh
  def write(a, b, c)
      @firewall = firewall(a)
      @firewall_on_startup = firewall_on_startup(b)
      @ssh = ssh(c)
  end

Again, firewall() and ssh() doing nearly the same. Depending on param, the scr.execute command will restart or stop the firewall/ssh. Any rc-command prints out a string like Shutting down the Firewall done, so go through the output and search for "done", if l.include? "done". If it succeeds, it returns the param. If not, firewall/ssh returns nil.

  def firewall(param)
    if param   # start firewall
      action = "restart"
    else       # stop firewall
      action = "stop"
    end
    cmd = @scr.execute(["/sbin/rcSuSEfirewall2", "#{action}"])
    lines = cmd[:stdout].split "\n"
    lines.each do |l|
      if l.length > 0
        if l.include? "done"
          return param
        end
      end
    end
    return nil
  end
  def ssh(param)
    if param    # start sshd
      action = "restart"
    else        # stop sshd
      action = "stop"
    end
    cmd = @scr.execute(["/usr/sbin/rcsshd", "#{action}"])
    lines = cmd[:stdout].split "\n"
    lines.each do |l|
      if l.length > 0
        if l.include? "done"
          return param
        end
      end
    end
    return nil
  end

The firewall_on_startup() method evaluates param and then decides how the action and the expected return should look like. Mention that scr.execute splits the respons of the command into stdout and stderr. That's why we use cmd[:stderr] to get the return value this time. Again, in case of success firewall_on_startup() returns the param and nil in case of a failure.

  def firewall_on_startup(param)
    if param    # 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 param
        end
      end
    end
    return nil
  end

Now we can go on with the controller.

The Controller

  def update
    unless permission_check("org.opensuse.yast.system.security.write")
      render ErrorResult.error(403, 1, "no permission") and return
    end

Like in the show action, check the user for the right to write security module first. Then check if the parameter has any values for security class. If it does, we can use the write() method of Security and hand over the parameter of params[:security][..].

    respond_to do |format|
      @security = Security.new
      if params[:security] != nil
        @security.write(params[:security][:firewall], params[:security]\
                      [:firewall_on_startup], params[:security][:ssh])
      else
        render ErrorResult.error(404, 2, "format or internal error") and return
      end

Finally the security object were rendered in xml or json, depending on the Http-request.

      format.html do
        render :xml => @security.to_xml( :root => "security",
                    :dasherize => false ), :location => "none"
      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
    logger.debug "UPDATED: #{@security.inspect}"
  end

As we already written the update action for the web-client controller, you can now restart client- and service-server and have a look at your new module.