개발노트: 현천 #9 관문

Bookmark and Share

관문(Kwanmun; 關門)은 현천 프로젝트의 통합 API Gateway 프로젝트로, 현천 프로젝트 내의 단위 Application에 대하여 인증/인가, 로깅/감사, 표준화된 인터페이스 및 공통 기능을 제공한다. 이 문서는 관문 Application의 기본 개발 과정을 기록한 것으로, 해당 시스템의 개발 초기 과정을 포함하여 Rail 4 기반의 개발 Tutorial을 겸한다.

저장소: https://github.com/hyeoncheon/kwanmun

시작하기

데이터베이스 사용자 생성

현천 프로젝트는 기본 DBMS로 PostgreSQL을 사용하며, 아래와 같이 사용자를 생성한다. (편의 상, 모든 하위 Application에서 공유하여 사용하기 위하여 사용자를 통일하였다.)

superhero@host:~$ sudo su - postgres
[sudo] password for superhero:
postgres@host:~$ psql
psql (9.3.6)
Type "help" for help.

postgres=# CREATE ROLE honcheonui WITH CREATEDB LOGIN PASSWORD 'p4ssw0rd';
CREATE ROLE
postgres=# \du
                              List of roles
 Role name  |                   Attributes                   | Member of
------------+------------------------------------------------+-----------
 honcheonui | Create DB                                      | {}
 postgres   | Superuser, Create role, Create DB, Replication | {}

postgres=# \q
postgres@host:~$ exit

기본 설정 - 뼈대 만들기

새 Rails4 App을 위한 Skeleton 작성. 이 부분은 특정 Application에 한정되는 부분이 아니고 Rail4의 일반적인 구성 방식이다.

새 애플리케이션 생성

평범한 rails new 명령을 통한 Skeleton 작성. bundle과 관련된 부분을 별도로 기록/관리하기 위하여 —skip-bundle 옵션을 사용하였고, -d 옵션으로 기본 데이터베이스를 지정하였다.

$ rails new kwanmun --skip-bundle -d postgresql
$ cd kwanmun/
$ git init
$ git add .
$ git commit -m "initialize app structure with 'rails new'"
hyeoncheon--kwanmun.jpg

기본값 설정

개발/배포에 연관된 파일을 저장소에서 분리하는 등, 나의 개발 스타일에 따른 설정 부분. (그런데 왜 이런 게 기본이 아닐까?)

환경설정을 위한 파일(application.yml)을 새로 만들고, 그것을 읽어 들일 initializer를 생성한다. 환경설정 파일은 저장소에 저장되지 않도록 Ignore 설정을 하고, 대신 저장소에는 .dist 파일을 template으로써 대신 넣는다.

$ cat >> .gitignore <<EOF
>
> ### custom entries
> /config/application.yml
> EOF
$ 
$ cat >> config/initializers/environment.rb <<EOF
> Rails.application.config.before_configuration do
>   env_file = File.join(Rails.root, 'config', 'application.yml')
>   YAML.load(File.open(env_file)).each do |key, value|
>     ENV[key.to_s] = value
>   end if File.exists?(env_file)
> end
> EOF
$ 
$ sed -i "s/username: yeoksa/username: <%= ENV['KWANMUN_DATABASE_USERNAME'] %>/" config/database.yml
$ vi config/database.yml
$ vi config/application.yml
$ vi config/application.yml.dist

기본 웹 프록시 서버로 사용할 unicorn 관련 Gem을 활성화하고, 다중 App 호스팅을 위해 prefix를 설정한다. (routing과 Asset Path)

$ sed -i "s/.*gem 'unicorn'/gem 'unicorn'/" Gemfile
$ vi config/application.rb
$ vi config/route.rb

최종 결과

최종적으로, 기본 Skeleton 이후에 다음과 같은 변경점이 만들어진다.

--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,6 @@
 /log/*
 !/log/.keep
 /tmp
+
+### custom entries
+/config/application.yml
--- a/Gemfile
+++ b/Gemfile
@@ -27,7 +27,7 @@ gem 'sdoc', '~> 0.4.0', group: :doc
 # gem 'bcrypt', '~> 3.1.7'
 
 # Use Unicorn as the app server
-# gem 'unicorn'
+gem 'unicorn'
 
 # Use Capistrano for deployment
 # gem 'capistrano-rails', group: :development
--- a/config/application.rb
+++ b/config/application.rb
@@ -20,7 +20,12 @@ module Kwanmun
     # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
     # config.i18n.default_locale = :de
 
+    # app specific config
+    config.relative_url_root = '/kwanmun'
+    config.assets.prefix = '/kwanmun/assets'
+
     # Do not swallow errors in after_commit/after_rollback callbacks.
     config.active_record.raise_in_transactional_callbacks = true
   end
 end
+# vim: set ts=2 sw=2 expandtab:
--- /dev/null
+++ b/config/application.yml.dist
@@ -0,0 +1,5 @@
+YEOKSA_DATABASE_HOSTNAME: @DATABASE_HOSTNAME@
+YEOKSA_DATABASE_USERNAME: @DATABASE_USERNAME@
+YEOKSA_DATABASE_PASSWORD: @DATABASE_PASSWORD@
+
+SECRET_KEY_BASE: @SECRET_KEY_BASE@
--- a/config/database.yml
+++ b/config/database.yml
@@ -20,6 +20,9 @@ default: &default
   # For details on connection pooling, see rails configuration guide
   # http://guides.rubyonrails.org/configuring.html#database-pooling
   pool: 5
+  host: <%= ENV['KWANMUN_DATABASE_HOSTNAME'] %>
+  username: <%= ENV['KWANMUN_DATABASE_USERNAME'] %>
+  password: <%= ENV['KWANMUN_DATABASE_PASSWORD'] %>
 
 development:
   <<: *default
@@ -81,5 +84,3 @@ test:
 production:
   <<: *default
   database: kwanmun_production
-  username: kwanmun
-  password: <%= ENV['KWANMUN_DATABASE_PASSWORD'] %>
--- /dev/null
+++ b/config/initializers/environment.rb
@@ -0,0 +1,6 @@
+Rails.application.config.before_configuration do
+  env_file = File.join(Rails.root, 'config', 'application.yml')
+  YAML.load(File.open(env_file)).each do |key, value|
+    ENV[key.to_s] = value
+  end if File.exists?(env_file)
+end
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,5 @@
 Rails.application.routes.draw do
+ scope '/kwanmun' do
   # The priority is based upon order of creation: first created -> highest priority.
   # See how all your routes lay out with "rake routes".
 
@@ -53,4 +54,5 @@ Rails.application.routes.draw do
   #     # (app/controllers/admin/products_controller.rb)
   #     resources :products
   #   end
+ end
 end

bundle 설치

이제, bundle 명령으로 필요한 Gem 설치를 진행하고,

$ bundle install
Fetching gem metadata from https://rubygems.org/............
Resolving dependencies...
Installing rake 10.4.2
Using i18n 0.7.0
Installing json 1.8.2
Using minitest 5.5.1
Using thread_safe 0.3.4
Using tzinfo 1.2.2
Using activesupport 4.2.0
Using builder 3.2.2
Using erubis 2.7.0
Using mini_portile 0.6.2
Using nokogiri 1.6.6.2
Using rails-deprecated_sanitizer 1.0.3
Using rails-dom-testing 1.0.5
Using loofah 2.0.1
Using rails-html-sanitizer 1.0.1
Using actionview 4.2.0
Using rack 1.6.0
Installing rack-test 0.6.3
Using actionpack 4.2.0
Using globalid 0.3.3
Using activejob 4.2.0
Installing mime-types 2.4.3
Installing mail 2.6.3
Using actionmailer 4.2.0
Using activemodel 4.2.0
Using arel 6.0.0
Using activerecord 4.2.0
Installing debug_inspector 0.0.2
Installing binding_of_caller 0.7.2
Using bundler 1.7.3
Installing coffee-script-source 1.9.1
Installing execjs 2.3.0
Installing coffee-script 2.3.0
Using thor 0.19.1
Using railties 4.2.0
Installing coffee-rails 4.1.0
Installing columnize 0.9.0
Installing debugger-linecache 1.2.0
Installing debugger-ruby_core_source 1.3.8
Installing debugger 1.6.8
Using hike 1.2.3
Using multi_json 1.10.1
Installing jbuilder 2.2.9
Installing jquery-rails 4.0.3
Installing kgio 2.9.3
Installing pg 0.18.1
Using tilt 1.4.1
Using sprockets 2.12.3
Using sprockets-rails 2.2.4
Using rails 4.2.0
Installing raindrops 0.13.0
Installing rdoc 4.2.0
Installing sass 3.4.13
Installing sass-rails 5.0.1
Installing sdoc 0.4.1
Installing spring 1.3.3
Installing turbolinks 2.5.3
Installing uglifier 2.7.1
Installing unicorn 4.8.3
Installing web-console 2.0.1
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.
$

Bundle gem 설치가 끝나면 다음과 같이 DBMS 접속 설정 확인 및 Migration 시험을 한다.

$ bin/rails runner 'puts ActiveRecord::Base.configurations'
{"default"=>{"adapter"=>"postgresql", "encoding"=>"unicode", "pool"=>5, "host"=>"localhost", "username"=>"honcheonui", "password"=>"p4ssw0rd"}, "development"=>{"adapter"=>"postgresql", "encoding"=>"unicode", "pool"=>5, "host"=>"localhost", "username"=>"honcheonui", "password"=>"p4ssw0rd", "database"=>"kwanmun_development"}, "test"=>{"adapter"=>"postgresql", "encoding"=>"unicode", "pool"=>5, "host"=>"localhost", "username"=>"honcheonui", "password"=>"p4ssw0rd", "database"=>"kwanmun_test"}, "production"=>{"adapter"=>"postgresql", "encoding"=>"unicode", "pool"=>5, "host"=>"localhost", "username"=>"honcheonui", "password"=>"p4ssw0rd", "database"=>"kwanmun_production"}}
$ 
$ rake db:setup RAILS_ENV=production KWANMUN_DATABASE_HOSTNAME=localhost KWANMUN_DATABASE_USERNAME=honcheonui KWANMUN_DATABASE_PASSWORD='p4ssw0rd'
/var/www/apps/kwanmun/db/schema.rb doesn't exist yet. Run `rake db:migrate` to create it, then try again. If you do not intend to use a database, you should instead alter /var/www/apps/yeoksa/config/application.rb to limit the frameworks that will be loaded.
$

Unicorn 설정 및 기동 확인

모든 준비가 완료되면 미리 만들어둔 표준 unicorn 설정파일을 넣어주고, /etc/unicorntab에 새 App을 추가한 후, nginx site 설정 추가.

$ cp ~/unicorn.rb config/
$ sudo vi /etc/unicorntab
$ sudo vi /etc/nginx/site-enabled/my_site
$ sudo service unicorn_multi restart
$ sudo service nginx restart

저장소 업데이트

모든 기본 설정 및 기동 시험이 완료되면, 해당 내용을 저장소에 Commit!

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   .gitignore
    modified:   Gemfile
    new file:   Gemfile.lock
    modified:   config/application.rb
    new file:   config/application.yml.dist
    modified:   config/database.yml
    new file:   config/initializers/environment.rb
    modified:   config/routes.rb
    new file:   config/unicorn.rb

$ git commit -m "basic skeleton completed"
[master ae355b6] basic skeleton completed
 9 files changed, 279 insertions(+), 4 deletions(-)
 create mode 100644 Gemfile.lock
 create mode 100644 config/application.yml.dist
 create mode 100644 config/initializers/environment.rb
 create mode 100644 config/unicorn.rb
$

App 작성

관문은 API Gateway이며, 사용자/서버로부터의 요청을 받은 후 인증 및 요청 내용의 허가 여부를 확인한 후, 실질적인 동작은 실제의 업무 서버로부터 일어나도록 중계하는 역할을 한다. 이와 함께, 어떤 사용자/서버가 어떤 요청을 했는지 등에 대한 기록을 함께 하게 되며, 이 과정에서 사용자(User), 서버(Server) 그리고 기록(Log) 등의 모델을 사용하게 된다.

핵심 요소 작성

App의 특성 상, 기본적으로 "중계"라는 하나의 업무를 수행하게 되며, MVC 구조에 있어서 View는 상대적으로 중요하지 않다. 인증 추가 과정에서 보다 구체화 되겠지만, 일단 기본 Model을 생성한다.

Server 모델 Scaffold 작성

API 상호 호출에 대한 기본 구상은 서비스(서버) 간 연동에 의한 경우를 가정하고 있다. 그러나 서버가 아닌 Application/사용자에 의한 경우도 고려하여 구성을 잡는다. 먼저, 다음과 같은 내용으로 서버의 틀을 잡는다. (서비스 간 연동이라고는 해도, 단일 서버 - 단일 서비스라는 보장이 없으므로 서버의 속성으로 서비스는 들어가지 않는다.)

$ rails g scaffold server uuid:string:uniq hostname:string address:string description:string api_key:string:uniq --no-javascripts --no-stylesheets
      invoke  active_record
      create    db/migrate/20150303090447_create_servers.rb
      create    app/models/server.rb
      invoke    test_unit
      create      test/models/server_test.rb
      create      test/fixtures/servers.yml
      invoke  resource_route
       route    resources :servers
      invoke  scaffold_controller
      create    app/controllers/servers_controller.rb
      invoke    erb
      create      app/views/servers
      create      app/views/servers/index.html.erb
      create      app/views/servers/edit.html.erb
      create      app/views/servers/show.html.erb
      create      app/views/servers/new.html.erb
      create      app/views/servers/_form.html.erb
      invoke    test_unit
      create      test/controllers/servers_controller_test.rb
      invoke    helper
      create      app/helpers/servers_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/servers/index.json.jbuilder
      create      app/views/servers/show.json.jbuilder
      invoke  assets
      invoke    coffee
      invoke    scss
$ vi config/routes.rb 
$ git add app db test config/routes.rb 
$ rake db:migrate RAILS_ENV=production
== 20150303090447 CreateServers: migrating ====================================
-- create_table(:servers)
   -> 0.0203s
-- add_index(:servers, :uuid, {:unique=>true})
   -> 0.0029s
-- add_index(:servers, :api_key, {:unique=>true})
   -> 0.0027s
== 20150303090447 CreateServers: migrated (0.0261s) ===========================

$ git commit -m "scaffold server"
$

Log 모델 Scaffold 작성

다음으로, 특정 서버 등으로부터의 요청에 대한 이력 기록과 함께 원격 로그 저장소의 역할을 함께 할 Log 모델을 만든다. 주의할 부분은, Server에 직접 관계를 거는 대신 "Client"라는 가상의 모델을 참고하도록 작성한다는 점이다.
(다형성 부여, client:references{polymorphic} 부분)

$ rails g scaffold log category:string level:string time:datetime service:string hostname:string process:string message:string actor:string action:string target:string reason:string tag:string client:references{polymorphic} --no-javascripts --no-stylesheets
      invoke  active_record
      create    db/migrate/20150303094040_create_logs.rb
      create    app/models/log.rb
      invoke    test_unit
      create      test/models/log_test.rb
      create      test/fixtures/logs.yml
      invoke  resource_route
       route    resources :logs
      invoke  scaffold_controller
      create    app/controllers/logs_controller.rb
      invoke    erb
      create      app/views/logs
      create      app/views/logs/index.html.erb
      create      app/views/logs/edit.html.erb
      create      app/views/logs/show.html.erb
      create      app/views/logs/new.html.erb
      create      app/views/logs/_form.html.erb
      invoke    test_unit
      create      test/controllers/logs_controller_test.rb
      invoke    helper
      create      app/helpers/logs_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/logs/index.json.jbuilder
      create      app/views/logs/show.json.jbuilder
      invoke  assets
      invoke    coffee
      invoke    scss
$ vi config/routes.rb
$ rake db:migrate RAILS_ENV=production
== 20150303094040 CreateLogs: migrating =======================================
-- create_table(:logs)
   -> 0.0087s
== 20150303094040 CreateLogs: migrated (0.0088s) ==============================

$ git add app db test config/routes.rb

다형성에 기반한 Log 모델이 Server 모델과 연관관계를 형성하기 위하여 다음과 같이 Server 모델의 속성을 수정한다.

--- a/app/models/server.rb
+++ b/app/models/server.rb
@@ -1,2 +1,3 @@
 class Server < ActiveRecord::Base
+  has_many :logs, as: :client
 end

이것까지 포함하여 Commit 한 번!

$ vi app/models/server.rb
$ git add app/models/server.rb
$ git commit -m "scaffold log"

참고로, 다음과 같이 다형성이 부여된 연관 관계가 정상적으로 동작하는지 간단하게 확인할 수 있다.

$ rails console
Loading development environment (Rails 4.2.0)
irb(main):001:0> s = Server.new
=> #<Server id: nil, uuid: nil, hostname: nil, address: nil, description: nil, api_key: nil, created_at: nil, updated_at: nil>
irb(main):002:0> l = s.logs.new
=> #<Log id: nil, category: nil, level: nil, time: nil, service: nil, hostname: nil, process: nil, message: nil, actor: nil, action: nil, target: nil, reason: nil, tag: nil, client_id: nil, client_type: "Server", created_at: nil, updated_at: nil>
irb(main):003:0> quit
$

SiSO 적용

대략적인 구조가 정비되었으므로 이번에는 "현천" 제품군의 공용 Single Sign On 서비스인 SiSO를 붙여준다. 자세한 과정은 다음 문서를 참고.

개발노트: 현천 #0 SiSO

User 모델 확장

표준 SiSO 설치 문서에 의해 인증 설정 및 User 모델의 생성을 마쳤다면, 다음과 같이 이 모델에게도 Log 모델에 대한 관계 설정을 해준다.

--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,2 +1,3 @@
 class User < ActiveRecord::Base
+  has_many :logs, as: :client
 end

Log 남기기

관문 App의 존재 목적은 사용자 및 API를 호출하는 Service/Server에 대한 중앙 통제 및 이력 관리이다. 이제 사용자 모델의 기본 모습이 갖춰졌으니 사용자의 접속 이력을 남기는 부분을 시작으로 본격적인 App 작성을 시작한다.

첫 번째 로깅: 사용자 접속 로그

아래 변경 이력의 첫 번째 파일은, 사용자와 상호작용하는 과정의 활동 기록을 남기기 위한 함수 부분이다. who, when, where 등에 해당하는 부분을 환경변수나 상황에 맞게 자동 설정하고, what, how, message 등의 주요 부분만 인수로 처리한다.

--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -16,5 +16,18 @@ class ApplicationController < ActionController::Base
       redirect_to signin_path
     end
   end
+
+  def activity_log(category, level, how, what, message, why='', tags='')
+    if current_user
+      who = current_user.name
+      now = Time.now
+      where = env['HTTP_X_FORWARDED_FOR']
+      process = env['HTTP_USER_AGENT']
+      current_user.logs.create(
+        category: category, level: level, time: now, service: 'kwanmun',
+        process: process, message: message, hostname: where,
+        actor: who, action: how, target: what, reason: why, tag: tags)
+    end
+  end
 end
 # vim: set ts=2 sw=2 expandtab:
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -12,6 +12,7 @@ class SessionsController < ApplicationController
                           siso_active: ai[:active],
                           name: ai[:name], mail: ai[:email], image: ai[:image])
       flash[:notice] = "New user for #{@user.mail} registered!"
+      activity_log 'auth', 'info', 'register uid', @user.id, flash[:notice]
     else
       # update user informations from siso.
       @user.siso_gid = ai[:gid]
@@ -21,6 +22,7 @@ class SessionsController < ApplicationController
       @user.image = ai[:image]
       @user.save
       flash[:notice] = "welcome #{@user.name}!"
+      activity_log 'auth', 'info', 'login uid', @user.id, flash[:notice]
     end
     session[:user] = @user.id
     session[:name] = @user.name

위의 두 번째 변경 파일에서 볼 수 있듯이, 적절한 장소에서 해당 함수를 호출하여 사용자 활동에 대한 로깅을 할 수 있으며, 이 내용은 Client로써의 User 모델에 연결된다. 그리고 Commit!

$ git commit -m "implement activity_log"
$

API 구성

Rails는 Application 접근 보안을 위해 Appliation Controller에 대하여 CSRF Token을 통한 보안 설정이 기본적으로 활성화 된다. 이 설정을 전체적으로 또는 Controller나 Method 단위로 끌 수 있지만, 부수적인 API 활용이 아닌 API가 중심이 이 Application의 경우에는 별도의 Controller를 만들고 UI와는 별도의 인증을 거치도록 하는 것이 효율적이다.

API 기본 구조

다음과 같은 방식으로 API 전용 Controller와 원격 Log 저장을 위한 Controller를 작성한다. 또한, generate 명령을 사용할 때 api/, api/v1/ 등의 Prefix를 사용하여 전용의 Namespace를 구성하고 API 버전 관리를 가능하도록 구성한다.

API 전용 Controller 구조 생성

먼저, 다음과 같이 기본 세트를 구성한다.

$ rails g controller api/api --no-javascripts --no-stylesheets
      create  app/controllers/api/api_controller.rb
      invoke  erb
      create    app/views/api/api
      invoke  test_unit
      create    test/controllers/api/api_controller_test.rb
      invoke  helper
      create    app/helpers/api/api_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      invoke    scss
$ 
$ rails g controller api/v1/logs --no-javascripts --no-stylesheets
      create  app/controllers/api/v1/logs_controller.rb
      invoke  erb
      create    app/views/api/v1/logs
      invoke  test_unit
      create    test/controllers/api/v1/logs_controller_test.rb
      invoke  helper
      create    app/helpers/api/v1/logs_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      invoke    scss
$

generate 명령에 의해 자동으로 생성된 Controller 는 Application Controller를 자동으로 상속하게 되므로, 아래와 같이 수정하여 api_controller는 API 일반 관리를 위한 Controller로 별도 관리하고, logs_controller 등 앞으로 추가될 Controller는 API Controller를 상속하여 그 특성을 따를 수 있도록 해준다.

--- a/app/controllers/api/api_controller.rb
+++ b/app/controllers/api/api_controller.rb
@@ -1,2 +1,2 @@
-class Api::ApiController < ApplicationController
+class Api::ApiController < ActionController::Base
 end
--- a/app/controllers/api/v1/logs_controller.rb
+++ b/app/controllers/api/v1/logs_controller.rb
@@ -1,2 +1,2 @@
-class Api::V1::LogsController < ApplicationController
+class Api::V1::LogsController < Api::ApiController
 end
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -8,6 +8,12 @@ Rails.application.routes.draw do
     resources :logs
   end
 
+  namespace :api, defaults: {format: 'json'} do
+    namespace :v1 do
+      resources :logs
+    end
+  end
+
   ### hyeoncheon.siso
   get '/auth/siso', as: :signin
   get '/auth/:provider/callback', to: 'sessions#create'

Log API 구현

기존 Contoller를 참고하여 CRUD에 해당하는 요청에 대해 JSON 응답을 할 수 있도록Log API를 구현하고 이에 따른 JSON Builder를 만들어 넣는다. 이 과정의 변경 내용을 요약하면 아래와 같다.

--- a/app/controllers/api/v1/logs_controller.rb
+++ b/app/controllers/api/v1/logs_controller.rb
@@ -1,2 +1,53 @@
 class Api::V1::LogsController < Api::ApiController
+  before_action :set_log, only: [:show, :edit, :update, :destroy]
+
+  # GET /logs.json
+  def index
+    @logs = Log.all
+  end
+
+  # GET /logs/1.json
+  def show
+  end
+
+  # POST /logs.json
+  def create
+    @log = Log.new(log_params)
+
+    respond_to do |format|
+      if @log.save
+        format.json { render :show, status: :created, location: @log }
+      else
+        format.json { render json: @log.errors, status: :unprocessable_entity }
+      end
+    end
+  end
+
+  # PATCH/PUT /logs/1.json
+  def update
+    respond_to do |format|
+      if @log.update(log_params)
+        format.json { render :show, status: :ok, location: @log }
+      else
+        format.json { render json: @log.errors, status: :unprocessable_entity }
+      end
+    end
+  end
+
+  # DELETE /logs/1.json
+  def destroy
+    @log.destroy
+    respond_to do |format|
+      format.json { head :no_content }
+    end
+  end
+
+  private
+    def set_log
+      @log = Log.find(params[:id])
+    end
+
+    def log_params
+      params.require(:log).permit(:category, :level, :time, :service, :hostname
+    end
 end

추가된 파일들을 저장소에 Commit!

$ git add app/controllers/api
$ git add app/views/api
$ git add config/routes.rb 
$ git commit -m "bare api structure"

API 동작 시험

이렇게 만들어진 API는 다음과 같이 명령행에서 시험해볼 수 있다.

$ curl -X POST -H "Content-Type: application/json" -d '{"log": {
    "category":"boot",
    "level":"info",
    "time":"2015-02-26 07:59:00",
    "service":"test.example.com",
    "hostname":"darkstar",
    "process":"kernel",
    "message":"system reboot",
    "actor":"system",
    "action":"reboot",
    "target":"system",
    "reason":"Planed Maintenance",
    "tag":"IR-93029"} }' \
    http://current.example.com/kwanmun/api/v1/logs
{"id":4,"category":"boot","level":"info","time":"2015-02-26T07:59:00.000Z","service":"test.skcc.com","hostname":"skcc-testdb1","process":"kernel","message":"system reboot","actor":"system","action":"reboot","target":"system","reason":"Planed Maintenance","tag":"IR-93029","client_id":null,"client_type":null,"created_at":"2015-03-03T16:00:42.889Z","updated_at":"2015-03-03T16:00:42.889Z"}
$

POSTMAN을 이용한 동작 확인

반복되는 시험을 편리하게 진행하기 위하여 script를 만들어 사용할 수도 있으며, GUI 환경을 선호한다면 다음과 같이 POSTMAN 등을 활용하여 시험할 수도 있다.

postman-kwanmun.png

API 인증

API 인증은 사용자의 Web Browser를 이용한 접근과는 달리 기계적으로 이루어지기 때문에 대화형 인증을 사용하는 것은 효과적이지 않다. HTTP Basic Auth 등을 이용할 수도 있지만 사용자/암호 기반의 인증이 갖는 한계가 있다.

사용자 접속의 경우, 다른 현천 Application과 같이 SiSO를 이용한 OAuth2 인등을 기본으로 사용하며, API의 경우에는 API Token 기반 인증을 사용하도록 구성한다.

Application Contoller 접근제어 강화

API 인증 적용에 앞서, 기본 Application Controller의 접근제어를 강화할 필요가 있다. SiSO 설정 문서를 보면, View Layer의 Layout 설정을 통하여 로그인 확인을 하도록 되어있지만, JSON 호출 등에 대해서는 무방비 상태이다.

다음과 같이, 전체 Application Controller에 대하여 login_required action을 걸어주고, 세션 인증을 위한 Session#Create에 대해서만 예외처리를 해줌으로써, 비정상적인 접근을 제어할 수 있다.

--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -2,6 +2,7 @@ class ApplicationController < ActionController::Base
   # Prevent CSRF attacks by raising an exception.
   # For APIs, you may want to use :null_session instead.
   protect_from_forgery with: :exception
+  before_action :login_required
 
   private
   def current_user
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,4 +1,6 @@
 class SessionsController < ApplicationController
+  before_action :login_required, except: :create
+
   def create
     omniauth = request.env['omniauth.auth']
     ai = omniauth[:info].clone

API Token 설정

API 인증은 API Key 기반으로 이루어지며, 아래와 같이 모델을 수정하여 Server나 User 모델을 생성할 때 자동으로 생성하도록 한다. SiSO 설정 과정에서 만들어진 User 모델에는 이에 해당하는 속성이 없으므로 추가해준다.

$ rails g migration AddApiKeyToUser api_key:string:uniq
      invoke  active_record
      create    db/migrate/20150303175147_add_api_key_to_user.rb
$ rake db:migrate RAILS_ENV=production
== 20150303175147 AddApiKeyToUser: migrating ==================================
-- add_column(:users, :api_key, :string)
   -> 0.0010s
-- add_index(:users, :api_key, {:unique=>true})
   -> 0.0037s
== 20150303175147 AddApiKeyToUser: migrated (0.0048s) =========================

$

자동생성 부분은 다음과 같이 내부 Method를 만들어서 사용한다.

--- a/app/models/server.rb
+++ b/app/models/server.rb
@@ -1,4 +1,12 @@
 class Server < ActiveRecord::Base
   has_many :logs, as: :client
+  before_create :generate_api_key
+
+  private
+  def generate_api_key
+    begin
+      self.api_key = Digest::SHA256.new.to_s
+    end while self.class.exists?(api_key: api_key)
+  end
 end
 # vim: set ts=2 sw=2 expandtab:
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,4 +1,12 @@
 class User < ActiveRecord::Base
   has_many :logs, as: :client
+  before_create :generate_api_key
+
+  private
+  def generate_api_key
+    begin
+      self.api_key = Digest::SHA256.new.to_s
+    end while self.class.exists?(api_key: api_key)
+  end
 end
 # vim: set ts=2 sw=2 expandtab:

추가로, 자동 또는 향후 수동 갱신으로 내부적 방식에 의해 만들어지는 API Key의 임의 변경을 막기 위해, 표준 Controller의 수정이 필요하다.

--- a/app/controllers/servers_controller.rb
+++ b/app/controllers/servers_controller.rb
@@ -69,6 +69,6 @@ class ServersController < ApplicationController
 
     # Never trust parameters from the scary internet, only allow the white list
     def server_params
-      params.require(:server).permit(:uuid, :hostname, :address, :description, 
+      params.require(:server).permit(:uuid, :hostname, :address, :description)
     end
 end

서버 자동 등록

SiSO 인증을 통하여 자동 생성되는 사용자와는 달리, 서버는 관리자가 개입하지 않으면 별도의 인증을 거칠 수 없다. 하지만, 손이 덜 가는 시스템으로 만들기 위해, 새로운 서버로부터의 API 요청 발생을 기준으로 Server를 자동으로 만들어주는 방식을 취하도록 한다. (단, 새로 생성되는 순간에 API Key를 자동으로 생성하기 때문에, 실제로는 이렇게 만들어진 서버가 API를 바로 이용하는 것은 불가능하다.)

서버가 API Key와 함께 자동으로 만들어진다는 점이 보안 상 문제가 있을 수도 있으나, 기본적으로 API Key 일치 확인을 통하여, 만들어진 Key 배포 전에는 실질적인 API 접근은 불가능하다는 점으로 보완한다.

Before Action을 이용한 서버 자동 등록 및 API Key(Token) 인증을 위한 변경은 아래와 같다.

--- a/app/controllers/api/api_controller.rb
+++ b/app/controllers/api/api_controller.rb
@@ -1,3 +1,14 @@
 class Api::ApiController < ActionController::Base
+  before_action :token_required
+
+  def token_required
+    client_addr = env['HTTP_X_FORWARDED_FOR']
+    unless @client = Server.find_by_address(client_addr)
+      @client = Server.create(address: client_addr)
+    end
+
+    authenticate_or_request_with_http_token do |token, options|
+      @client.is_authorized?(token)
+    end
 end
 # vim: set ts=2 sw=2 expandtab:
--- a/app/controllers/api/v1/logs_controller.rb
+++ b/app/controllers/api/v1/logs_controller.rb
@@ -12,7 +12,7 @@ class Api::V1::LogsController < Api::ApiController
 
   # POST /logs.json
   def create
-    @log = Log.new(log_params)
+    @log = @client.logs.new(log_params)
 
     respond_to do |format|
       if @log.save
--- a/app/models/server.rb
+++ b/app/models/server.rb
@@ -2,6 +2,10 @@ class Server < ActiveRecord::Base
   has_many :logs, as: :client
   before_create :generate_api_key
 
+  def is_authorized? token
+    self.api_key == token
+  end
+
   private
   def generate_api_key
     begin

기타 Pholymorphic 표준 함수 등의 설정을 추가하고, Commit! 하면, 원격 로그 저장소는 완성!

$ git commit -m "api token authentication"
$

참고: 선행 설치

Rail 최신버전 사용을 위한 설치

현재버전의 Rails 는 4.2.0이며, 이미 설치되어있는 버전 대신 새 버전을 사용하기 위하여 다음과 같이 추가 설치를 진행

Rails Gem 설치

$ sudo gem install rails
[sudo] password for superhero:
Fetching: i18n-0.7.0.gem (100%)
Fetching: thread_safe-0.3.4.gem (100%)
Fetching: tzinfo-1.2.2.gem (100%)
Fetching: minitest-5.5.1.gem (100%)
Fetching: activesupport-4.2.0.gem (100%)
Fetching: rack-1.6.0.gem (100%)
Fetching: mini_portile-0.6.2.gem (100%)
Fetching: nokogiri-1.6.6.2.gem (100%)
Building native extensions.  This could take a while...
Fetching: loofah-2.0.1.gem (100%)
Fetching: rails-html-sanitizer-1.0.1.gem (100%)
Fetching: rails-deprecated_sanitizer-1.0.3.gem (100%)
Fetching: rails-dom-testing-1.0.5.gem (100%)
Fetching: builder-3.2.2.gem (100%)
Fetching: actionview-4.2.0.gem (100%)
Fetching: actionpack-4.2.0.gem (100%)
Fetching: activemodel-4.2.0.gem (100%)
Fetching: arel-6.0.0.gem (100%)
Fetching: activerecord-4.2.0.gem (100%)
Fetching: globalid-0.3.3.gem (100%)
Fetching: activejob-4.2.0.gem (100%)
Fetching: actionmailer-4.2.0.gem (100%)
Fetching: railties-4.2.0.gem (100%)
Fetching: sprockets-2.12.3.gem (100%)
Fetching: sprockets-rails-2.2.4.gem (100%)
Fetching: rails-4.2.0.gem (100%)
Successfully installed i18n-0.7.0
Successfully installed thread_safe-0.3.4
Successfully installed tzinfo-1.2.2
Successfully installed minitest-5.5.1
Successfully installed activesupport-4.2.0
Successfully installed rack-1.6.0
Successfully installed mini_portile-0.6.2
Successfully installed nokogiri-1.6.6.2
Successfully installed loofah-2.0.1
Successfully installed rails-html-sanitizer-1.0.1
Successfully installed rails-deprecated_sanitizer-1.0.3
Successfully installed rails-dom-testing-1.0.5
Successfully installed builder-3.2.2
Successfully installed actionview-4.2.0
Successfully installed actionpack-4.2.0
Successfully installed activemodel-4.2.0
Successfully installed arel-6.0.0
Successfully installed activerecord-4.2.0
Successfully installed globalid-0.3.3
Successfully installed activejob-4.2.0
Successfully installed actionmailer-4.2.0
Successfully installed railties-4.2.0
Successfully installed sprockets-2.12.3
Successfully installed sprockets-rails-2.2.4
Successfully installed rails-4.2.0
25 gems installed
Installing ri documentation for i18n-0.7.0...
Installing ri documentation for thread_safe-0.3.4...
Installing ri documentation for tzinfo-1.2.2...
Installing ri documentation for minitest-5.5.1...
Installing ri documentation for activesupport-4.2.0...
Installing ri documentation for rack-1.6.0...
Installing ri documentation for mini_portile-0.6.2...
Installing ri documentation for nokogiri-1.6.6.2...
Installing ri documentation for loofah-2.0.1...
Installing ri documentation for rails-html-sanitizer-1.0.1...
Installing ri documentation for rails-deprecated_sanitizer-1.0.3...
Installing ri documentation for rails-dom-testing-1.0.5...
Installing ri documentation for builder-3.2.2...
Installing ri documentation for actionview-4.2.0...
Installing ri documentation for actionpack-4.2.0...
Installing ri documentation for activemodel-4.2.0...
Installing ri documentation for arel-6.0.0...
Installing ri documentation for activerecord-4.2.0...
Installing ri documentation for globalid-0.3.3...
Installing ri documentation for activejob-4.2.0...
Installing ri documentation for actionmailer-4.2.0...
Installing ri documentation for railties-4.2.0...
Installing ri documentation for sprockets-2.12.3...
Installing ri documentation for sprockets-rails-2.2.4...
Installing ri documentation for rails-4.2.0...
Installing RDoc documentation for i18n-0.7.0...
Installing RDoc documentation for thread_safe-0.3.4...
Installing RDoc documentation for tzinfo-1.2.2...
Installing RDoc documentation for minitest-5.5.1...
Installing RDoc documentation for activesupport-4.2.0...
Installing RDoc documentation for rack-1.6.0...
Installing RDoc documentation for mini_portile-0.6.2...
Installing RDoc documentation for nokogiri-1.6.6.2...
Installing RDoc documentation for loofah-2.0.1...
Installing RDoc documentation for rails-html-sanitizer-1.0.1...
Installing RDoc documentation for rails-deprecated_sanitizer-1.0.3...
Installing RDoc documentation for rails-dom-testing-1.0.5...
Installing RDoc documentation for builder-3.2.2...
Installing RDoc documentation for actionview-4.2.0...
Installing RDoc documentation for actionpack-4.2.0...
Installing RDoc documentation for activemodel-4.2.0...
Installing RDoc documentation for arel-6.0.0...
Installing RDoc documentation for activerecord-4.2.0...
Installing RDoc documentation for globalid-0.3.3...
Installing RDoc documentation for activejob-4.2.0...
Installing RDoc documentation for actionmailer-4.2.0...
Installing RDoc documentation for railties-4.2.0...
Installing RDoc documentation for sprockets-2.12.3...
Installing RDoc documentation for sprockets-rails-2.2.4...
Installing RDoc documentation for rails-4.2.0...
ERROR:  While generating documentation for rails-4.2.0
... MESSAGE:   error generating /var/lib/gems/1.9.1/doc/rails-4.2.0/rdoc/guides/Rakefile.html: Error while evaluating /var/lib/gems/1.9.1/gems/rdoc-3.12.2/lib/rdoc/generator/template/darkfish/page.rhtml: undefined method `chomp' for nil:NilClass (RDoc::Error)
... RDOC args: --op /var/lib/gems/1.9.1/doc/rails-4.2.0/rdoc lib --title rails-4.2.0 Documentation --quiet
$

JavaScript Runtime for ExecJS (for rake)

$ sudo apt-get install nodejs
<...>
다음 새 패키지를 설치할 것입니다:
  libc-ares2 libv8-3.14.5 nodejs
<...>
$

Bookmark and Share


따로 명시하지 않는 한에서 이 사이트의 모든 콘텐츠는 다음의 라이선스를 따릅니다: Creative Commons Attribution-NonCommercial 3.0 License