
Security Fundamentals
Turtles, Clams, and Cyber Threat Actors: Shell Usage
The Socket Threat Research Team uncovers how threat actors weaponize shell techniques across npm, PyPI, and Go ecosystems to maintain persistence and exfiltrate data.
It's a Rails engine that gathers a set of gems and configurations designed to build incredible REST APIs.
These gems are:
To understand what this gem does, it is recommended to read first about those mentioned above.
Add to your Gemfile:
gem 'power_api'
group :development, :test do
gem 'factory_bot_rails'
gem 'rspec-rails'
gem 'rswag-specs'
gem 'rubocop'
gem 'rubocop-rspec'
end
Then,
bundle install
You must run the following command to have the initial configuration:
rails generate power_api:install
After doing this you will get:
A base controller for your API under /your_app/app/controllers/api/base_controller.rb
class Api::BaseController < PowerApi::BaseController
end
Here you should include everything common to all your APIs. It is usually empty because most of the configuration comes in the PowerApi::BaseController
that is inside the gem.
Some initializers:
/your_api/config/initializers/active_model_serializers.rb
:
ActiveModelSerializers.config.adapter = :json
/your_api/config/initializers/api_pagination.rb
:
ApiPagination.configure do |config|
config.paginator = :kaminari
# more options...
end
We use what comes by default and kaminari as pager.
After running the installer you must choose an API mode.
Use this mode if your API will be accessed by multiple clients or if your API is served somewhere other than your client application.
You must run the following command to have the exposed API mode configuration:
rails generate power_api:exposed_api_config
After doing this you will get:
A base controller for the first version of your API under /your_api/app/controllers/api/exposed/v1/base_controller.rb
class Api::Exposed::V1::BaseController < Api::BaseController
before_action do
self.namespace_for_serializer = ::Api::Exposed::V1
end
end
Everything related to version 1 of your API must be included here.
Some initializers: We configure the first version to be seen in the documentation view.
/your_api/config/initializers/simple_token_authentication.rb
:
SimpleTokenAuthentication.configure do |config|
# options...
end
We use the default options.A modified /your_api/config/routes.rb
file:
Rails.application.routes.draw do
scope path: '/api' do
api_version(module: 'Api::Exposed::V1', path: { value: 'v1' }, defaults: { format: 'json' }) do
end
end
# ...
end
--authenticated-resources
Use this option if you want to configure Simple Token Authentication for one or more models.
rails g power_api:install --authenticated-resources=user
Running the above code will generate, in addition to everything described in the initial setup, the following:
The Simple Token Authentication initializer /your_api/config/initializers/simple_token_authentication.rb
An edited version of the User model with the configuration needed for Simple Token Authentication.
class User < ApplicationRecord
acts_as_token_authenticatable
# more code...
end
The migration /your_api/db/migrate/20200228173608_add_authentication_token_to_users.rb
to add the authentication_token
to your users table.
Use this mode if your API and your client app will be served on the same place.
You must run the following command to have the internal API mode configuration:
rails generate power_api:internal_api_config
After doing this you will get:
A base controller for your internal API under /your_api/app/controllers/api/internal/base_controller.rb
class Api::Internal::BaseController < Api::BaseController
before_action do
self.namespace_for_serializer = ::Api::Internal
end
end
Anything shared by the internal API controllers should go here.
A modified /your_api/config/routes.rb
file:
namespace :api, defaults: { format: :json } do
namespace :internal do
end
end
An empty directory indicating where you should put your serializers: /your_api/app/serializers/api/internal/.gitkeep
To add a new version you must run the following command:
rails g power_api:version VERSION_NUMBER
Example:
rails g power_api:version 2
Doing this will add the same thing that was added for version one in the initial setup but this time for the number version provided as parameter.
To add a controller you must run the following command:
rails g power_api:controller MODEL_NAME [options]
Example:
rails g power_api:controller blog
Assuming we have the following model,
class Blog < ApplicationRecord
# == Schema Information
#
# Table name: blogs
#
# id :bigint(8) not null, primary key
# title :string(255)
# body :text(65535)
# created_at :datetime not null
# updated_at :datetime not null
#
end
after doing this you will get:
A modified /your_api/config/routes.rb
file with the new resource:
Rails.application.routes.draw do
scope path: '/api' do
api_version(module: 'Api::V1', path: { value: 'v1' }, defaults: { format: 'json' }) do
resources :blogs
end
end
end
Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
namespace :internal do
resources :blogs
end
end
end
A controller under /your_api/app/controllers/api/exposed/v1/blogs_controller.rb
class Api::Exposed::V1::BlogsController < Api::Exposed::V1::BaseController
def index
respond_with Blog.all
end
def show
respond_with blog
end
def create
respond_with Blog.create!(blog_params)
end
def update
respond_with blog.update!(blog_params)
end
def destroy
respond_with blog.destroy!
end
private
def blog
@blog ||= Blog.find_by!(id: params[:id])
end
def blog_params
params.require(:blog).permit(
:id,
:title,
:body,
)
end
end
With internal mode the file path will be:
/your_api/app/controllers/api/internal/blogs_controller.rb
and the class name:Api::Internal::BlogsController
A serializer under /your_api/app/serializers/api/exposed/v1/blog_serializer.rb
class Api::Exposed::V1::BlogSerializer < ActiveModel::Serializer
type :blog
attributes(
:id,
:title,
:body,
:created_at,
:updated_at
)
end
With internal mode the file path will be:
/your_api/app/serializers/api/internal/blog_serializer.rb
and the class name:Api::Internal::BlogSerializer
A spec file under /your_api/spec/integration/api/exposed/v1/blogs_spec.rb
require 'rails_helper'
RSpec.describe 'Api::Exposed::V1::BlogsControllers', type: :request do
describe 'GET /index' do
let!(:blogs) { create_list(:blog, 5) }
let(:collection) { JSON.parse(response.body)['blogs'] }
let(:params) { {} }
def perform
get '/api/v1/blogs', params: params
end
before do
perform
end
it { expect(collection.count).to eq(5) }
it { expect(response.status).to eq(200) }
end
describe 'POST /create' do
let(:params) do
{
blog: {
title: 'Some title',
body: 'Some body'
}
}
end
let(:attributes) do
JSON.parse(response.body)['blog'].symbolize_keys
end
def perform
post '/api/v1/blogs', params: params
end
before do
perform
end
it { expect(attributes).to include(params[:blog]) }
it { expect(response.status).to eq(201) }
context 'with invalid attributes' do
let(:params) do
{
blog: {
title: nil
}
}
end
it { expect(response.status).to eq(400) }
end
end
describe 'GET /show' do
let(:blog) { create(:blog) }
let(:blog_id) { blog.id.to_s }
let(:attributes) do
JSON.parse(response.body)['blog'].symbolize_keys
end
def perform
get '/api/v1/blogs/' + blog_id
end
before do
perform
end
it { expect(response.status).to eq(200) }
context 'with resource not found' do
let(:blog_id) { '666' }
it { expect(response.status).to eq(404) }
end
end
describe 'PUT /update' do
let(:blog) { create(:blog) }
let(:blog_id) { blog.id.to_s }
let(:params) do
{
blog: {
title: 'Some title',
body: 'Some body'
}
}
end
let(:attributes) do
JSON.parse(response.body)['blog'].symbolize_keys
end
def perform
put '/api/v1/blogs/' + blog_id, params: params
end
before do
perform
end
it { expect(attributes).to include(params[:blog]) }
it { expect(response.status).to eq(200) }
context 'with invalid attributes' do
let(:params) do
{
blog: {
title: nil
}
}
end
it { expect(response.status).to eq(400) }
end
context 'with resource not found' do
let(:blog_id) { '666' }
it { expect(response.status).to eq(404) }
end
end
describe 'DELETE /destroy' do
let(:blog) { create(:blog) }
let(:blog_id) { blog.id.to_s }
def perform
delete '/api/v1/blogs/' + blog_id
end
before do
perform
end
it { expect(response.status).to eq(204) }
context 'with resource not found' do
let(:blog_id) { '666' }
it { expect(response.status).to eq(404) }
end
end
end
With internal mode the file path will be:
your_api/spec/integration/api/internal/blogs_spec.rb
and the class name:Api::Internal::BlogsControllers
--attributes
Use this option if you want to choose which attributes of your model to add to the API response.
rails g power_api:controller blog --attributes=title
When you do this, you will see permited_params, serializers, etc. showing only the selected attributes
For example, the serializer under /your_api/app/serializers/api/exposed/v1/blog_serializer.rb
will show:
class Api::Exposed::V1::BlogSerializer < ActiveModel::Serializer
type :blog
attributes(
:title,
)
end
--controller-actions
Use this option if you want to choose which actions will be included in the controller.
rails g power_api:controller blog --controller-actions=show destroy
When you do this, you will see that only relevant code is generated in controller, tests and routes.
For example, the controller would only include the show
and destroy
actions and wouldn't include the blog_params
method:
class Api::Exposed::V1::BlogController < Api::Exposed::V1::BaseController
def show
respond_with blog
end
def destroy
respond_with blog.destroy!
end
private
def blog
@blog ||= Blog.find_by!(id: params[:id])
end
end
--version-number
Use this option if you want to decide which version the new controller will belong to.
rails g power_api:controller blog --version-number=2
Important! When working with exposed api you should always specify the version, otherwise the controller will be generated for the internal api mode.
--use-paginator
Use this option if you want to paginate the index endpoint collection.
rails g power_api:controller blog --use-paginator
The controller under /your_api/app/controllers/api/exposed/v1/blogs_controller.rb
will be modified to use the paginator like this:
class Api::Exposed::V1::BlogsController < Api::Exposed::V1::BaseController
def index
respond_with paginate(Blog.all)
end
# more code...
end
Due to the API Pagination gem the X-Total
, X-Per-Page
and X-Page
headers will be added to the answer. The parameters params[:page][:number]
and params[:page][:size]
can also be passed through the query string to access the different pages.
Because the AMS gem is set with "json api" format, links related to pagination will be added to the API response.
--allow-filters
Use this option if you want to filter your index endpoint collection with Ransack
rails g power_api:controller blog --allow-filters
The controller under /your_api/app/controllers/api/exposed/v1/blogs_controller.rb
will be modified like this:
class Api::Exposed::V1::BlogsController < Api::Exposed::V1::BaseController
def index
respond_with filtered_collection(Blog.all)
end
# more code...
end
The filtered_collection
method is defined inside the gem and uses ransack below.
You will be able to filter the results according to this: https://github.com/activerecord-hackery/ransack#search-matchers
For example:
http://localhost:3000/api/v1/blogs?q[id_gt]=22
to search blogs with id greater than 22
--authenticate-with
Use this option if you want to have authorized resources.
To learn more about the authentication method used please read more about Simple Token Authentication gem.
rails g power_api:controller MODEL_NAME --authenticate-with=ANOTHER_MODEL_NAME
Example:
rails g power_api:controller blog --authenticate-with=user
When you do this your controller will have the following line:
class Api::Exposed::V1::BlogsController < Api::Exposed::V1::BaseController
acts_as_token_authentication_handler_for User, fallback: :exception
# mode code...
end
With internal mode a
before_action :authenticate_user!
statement will be added instead ofacts_as_token_authentication_handler_for
in order to work with devise gem directly.
In addition, the specs under /your_api/spec/integration/api/v1/blogs_spec.rb
will add tests related with authorization.
response '401', 'user unauthorized' do
let(:user_token) { 'invalid' }
run_test!
end
--owned-by-authenticated-resource
If you have an authenticated resource you can choose your new resource be owned by the authenticated one.
rails g power_api:controller blog --authenticate-with=user --owned-by-authenticated-resource
The controller will look like this:
class Api::Exposed::V1::BlogsController < Api::Exposed::V1::BaseController
acts_as_token_authentication_handler_for User, fallback: :exception
def index
respond_with blogs
end
def show
respond_with blog
end
def create
respond_with blogs.create!(blog_params)
end
def update
respond_with blog.update!(blog_params)
end
def destroy
respond_with blog.destroy!
end
private
def blog
@blog ||= blogs.find_by!(id: params[:id])
end
def blogs
@blogs ||= current_user.blogs
end
def blog_params
params.require(:blog).permit(
:id,
:title,
:body
)
end
end
As you can see the resource (blog
) will always come from the authorized one (current_user.blogs
)
To make this possible, the models should be related as follows:
class Blog < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :blogs
end
--parent-resource
Assuming we have the following models,
class Blog < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :blog
end
we can run the following code to handle nested resources:
rails g power_api:controller comment --attributes=body --parent-resource=blog
Running the previous code we will get:
The controller under /your_api/app/controllers/api/exposed/v1/comments_controller.rb
:
class Api::Exposed::V1::CommentsController < Api::Exposed::V1::BaseController
def index
respond_with comments
end
def show
respond_with comment
end
def create
respond_with comments.create!(comment_params)
end
def update
respond_with comment.update!(comment_params)
end
def destroy
respond_with comment.destroy!
end
private
def comment
@comment ||= Comment.find_by!(id: params[:id])
end
def comments
@comments ||= blog.comments
end
def blog
@blog ||= Blog.find_by!(id: params[:blog_id])
end
def comment_params
params.require(:comment).permit(
:id,
:body
)
end
end
As you can see the comments
used on index
and create
will always come from blog
(the parent resource)
A modified /your_api/config/routes.rb
file with the nested resource:
Rails.application.routes.draw do
scope path: '/api' do
api_version(module: 'Api::V1', path: { value: 'v1' }, defaults: { format: 'json' }) do
resources :comments, only: [:show, :update, :destroy]
resources :blogs do
resources :comments, only: [:index, :create]
end
end
end
end
Note that the options:
--parent-resource
and--owned-by-authenticated-resource
cannot be used together.
module PowerApi
class BaseController < ApplicationController
include Api::Error
include Api::Deprecated
self.responder = ApiResponder
respond_to :json
end
end
The PowerApi::BaseController
class that exists inside this gem and is inherited by the base class of your API (/your_app/app/controllers/api/base_controller.rb
) includes functionality that I will describe bellow:
Api::Error
concernThis module handles common exceptions like:
ActiveRecord::RecordNotFound
ActiveModel::ForbiddenAttributesError
ActiveRecord::RecordInvalid
PowerApi::InvalidVersion
Exception
If you want to handle new errors, this can be done by calling the respond_api_error
method in the base class of your API like this:
class Api::BaseController < PowerApi::BaseController
rescue_from "MyCustomErrorClass" do |exception|
respond_api_error(:bad_request, message: "some error message", detail: exception.message)
end
end
Api::Deprecated
concernThis module is useful when you want to mark endpoints as deprecated.
For example, if you have the following controller:
class Api::Exposed::V1::CommentsController < Api::Exposed::V1::BaseController
deprecate :index
def index
respond_with comments
end
# more code...
end
And then in your browser you execute: GET /api/v1/comments
, you will get a Deprecated: true
response header.
This is useful to notify your customers that an endpoint will not be available in the next version of the API.
ApiResponder
It look like this:
class ApiResponder < ActionController::Responder
def api_behavior
raise MissingRenderer.new(format) unless has_renderer?
if delete?
head :no_content
elsif post?
display resource, status: :created
else
display resource
end
end
end
As you can see, this simple Responder handles the API response based on the HTTP verbs.
PowerApi::ApplicationHelper#serialize_resource
helper methodThis helper method is useful if you want to serialize ActiveRecord resources to use in your views. For example, you can do:
<pre>
<%= serialize_resource(@resource, @options) %>
</pre>
To get:
{"id":1,"title":"lean","body":"bla","createdAt":"2022-01-08T18:15:46.624Z","updatedAt":"2022-01-08T18:15:46.624Z","portfolioId":null}
The @resource
parameter must be an ActiveRecord instance (ApplicationRecord
) or collection (ActiveRecord_Relation
).
The @options
parameter must be a Hash
and can contain the options you commonly use with Active Model Serializer gem (fields
, transform_key
, etc.) and some others:
include_root
: to get something like: {"id":1,"title":"lean"}
or {"blog": {"id":1,"title":"lean"}}
.output_format
: can be :hash
or :json
.To run the specs you need to execute, in the root path of the gem, the following command:
bundle exec guard
You need to put all your tests in the /power_api/spec/dummy/spec/
directory.
On master/main branch...
VERSION
in lib/power_api/version.rb
.Unreleased
title to current version in CHANGELOG.md
.bundle install
.Releasing v0.1.0
.git tag v0.1.0
.git push origin v0.1.0
.git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)Thank you contributors!
Power API is maintained by platanus.
Power API is © 2022 platanus, spa. It is free software and may be redistributed under the terms specified in the LICENSE file.
FAQs
Unknown package
We found that power_api demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security Fundamentals
The Socket Threat Research Team uncovers how threat actors weaponize shell techniques across npm, PyPI, and Go ecosystems to maintain persistence and exfiltrate data.
Security News
At VulnCon 2025, NIST scrapped its NVD consortium plans, admitted it can't keep up with CVEs, and outlined automation efforts amid a mounting backlog.
Product
We redesigned our GitHub PR comments to deliver clear, actionable security insights without adding noise to your workflow.