Skip to content

Commit

Permalink
api: added a bootstrap endpoint
Browse files Browse the repository at this point in the history
This new endpoint lives inside of the user namespace and its main goal is to
allow people to create the first admin user from the API. This endpoint is only
allowed if the `first_user_admin` option has not been disabled. Another
restriction is that there shouldn't be a user already created. Last but not
least, this method can be called without being authenticated.

In the ideal case, you would call this method when you just started your Portus
instance. Then, you will get the first user created
(same arguments as `POST /api/v1/users`) and it will also get an application
token assigned to it. The reponse will be the `plain_token` from the application
token. Thus, the whole point of this method is to follow this workflow:

1. Portus instance deployed.
2. Admin uses this bootstrap endpoint to create the admin user and get an
   application token.
3. With this application token the admin starts to administrate the
   instance (e.g. register the registry on Portus) entirely from the API.
4. Admin makes the Portus instance available inside of the organization and
   starts structuring it inside of Portus.

Finally, this commit also started to make error responses more uniform. This was
firstly done to DRY some of the code created by this feature. It probably needs
more work (see SUSE#1437).

See SUSE#1412

Signed-off-by: Miquel Sabaté Solà <msabate@suse.com>
  • Loading branch information
mssola committed Mar 15, 2018
1 parent 73162e3 commit f0432d1
Show file tree
Hide file tree
Showing 11 changed files with 387 additions and 184 deletions.
16 changes: 11 additions & 5 deletions config/config.yml
Expand Up @@ -163,12 +163,18 @@ oauth:
# Only members of team can sign in/up with Bitbucket. Need permission to read team membership.
team: ""

# Set first_user_admin to true if you want that the first user that signs up
# to be an admin.
# When enabled (default value), the first users to be created on the UI will be
# a Portus admin. If you disable this, then, in order to set the admin user, you
# will need to run: rake portus:make_admin[USERNAME].
#
# Set to false otherwise. Then you will need to run
# rake portus:make_admin[USERNAME]
# in order to set the admin user
# Moreover, if you set this option to false, then the POST
# /api/v1/users/bootstrap endpoint will be disabled (since it will try to create
# the first user as an administrator).
#
# Thus, only set this option to false if you are really sure that you have
# direct access to your Portus instance and it can be reached by other people on
# your network. Otherwise, leave the default value and create your first admin
# user right away (either through the API or the UI).
first_user_admin:
enabled: true

Expand Down
17 changes: 6 additions & 11 deletions lib/api/helpers.rb
@@ -1,11 +1,16 @@
# frozen_string_literal: true

require "portus/auth_from_token"
require "api/helpers/errors"
require "api/helpers/namespaces"

module API
module Helpers
include ::Portus::AuthFromToken

include Errors
include Namespaces

# On success it will fill the @user instance variable with the currently
# authenticated user for the API. Otherwise it will raise:
#
Expand All @@ -21,7 +26,7 @@ def authorization!(force_admin: true)

current_user

error!("Authentication fails.", 401) unless @user
unauthorized!("Authentication fails") unless @user
raise Pundit::NotAuthorizedError if force_admin && !@user.admin
end

Expand Down Expand Up @@ -62,15 +67,5 @@ def permitted_params
include_missing: false,
include_parent_namespaces: false)
end

# Helpers for namespaces
module Namespaces
# Returns an aggregate of the accessible namespaces for the current user.
def accessible_namespaces
special = Namespace.special_for(current_user).order(created_at: :asc)
normal = policy_scope(Namespace).order(created_at: :asc)
special + normal
end
end
end
end
33 changes: 33 additions & 0 deletions lib/api/helpers/application_tokens.rb
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module API
module Helpers
# Helpers regarding the management of authentication tokens. This module
# mostly contains methods that are shared across different paths.
module ApplicationTokens
# Create an application token for the given user with the given
# ID. The `params` parameter contains the parameters to be passed to the
# `ApplicationToken.create_token` method as `params`.
#
# This method already sends the proper HTTP response and code.
def create_application_token!(user, id, params)
if user.valid?
application_token, plain_token = ApplicationToken.create_token(
current_user: user,
user_id: id,
params: params
)

if application_token.errors.empty?
status 201
{ plain_token: plain_token }
else
bad_request!(application_token.errors)
end
else
bad_request!(user.errors)
end
end
end
end
end
58 changes: 58 additions & 0 deletions lib/api/helpers/errors.rb
@@ -0,0 +1,58 @@

# frozen_string_literal: true

require "portus/auth_from_token"

module API
module Helpers
# Errors implements helper methods for API error responses.
module Errors
def api_error!(code:, messages:)
obj = messages.is_a?(String) ? [messages] : messages
error!(obj, code)
end

# Sends a `400 Bad Request` error with a possible message as the response
# body.
def bad_request!(msg = "Bad Request")
api_error!(code: 400, messages: msg)
end

# Sends a `401 Unauthorized` error with a possible message as the response
# body.
def unauthorized!(msg = "Unauthorized")
api_error!(code: 401, messages: msg)
end

# Sends a `403 Forbidden` error with a possible message as the response
# body.
def forbidden!(msg = "Forbidden")
api_error!(code: 403, messages: msg)
end

# Sends a `404 Not found` error with a possible message as the response
# body.
def not_found!(msg = "Not found")
api_error!(code: 404, messages: msg)
end

# Sends a `405 Method Not Allowed` error with a possible message as the
# response body.
def method_not_allowed!(msg = "Method Not Allowed")
api_error!(code: 405, messages: msg)
end

# Sends a `422 Unprocessable Entity` error with a possible message as the
# response body.
def unprocessable_entity!(msg = "Unprocessable Entity")
api_error!(code: 422, messages: msg)
end

# Sends a `405 Internal Server Error` error with a possible message as the
# response body.
def internal_server_error!(msg = "Internal Server Error")
api_error!(code: 500, messages: msg)
end
end
end
end
15 changes: 15 additions & 0 deletions lib/api/helpers/namespaces.rb
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module API
module Helpers
# Helpers for namespaces
module Namespaces
# Returns an aggregate of the accessible namespaces for the current user.
def accessible_namespaces
special = Namespace.special_for(current_user).order(created_at: :asc)
normal = policy_scope(Namespace).order(created_at: :asc)
special + normal
end
end
end
end
17 changes: 11 additions & 6 deletions lib/api/root_api.rb
Expand Up @@ -26,26 +26,31 @@ class RootAPI < Grape::API
end

rescue_from ActiveRecord::RecordNotFound do
error_response message: "Not found", status: 404
not_found!
end

rescue_from Grape::Exceptions::MethodNotAllowed do |e|
error_response message: { errors: e.message }, status: 405
method_not_allowed!(e.message)
end

rescue_from Grape::Exceptions::ValidationErrors do |e|
error_response message: { errors: e.errors }, status: 400
bad_request!(e.errors)
end

rescue_from Pundit::NotAuthorizedError do |_|
error_response message: { errors: "Authorization fails" }, status: 403
forbidden!("Authorization fails")
end

# global exception handler, used for error notifications
rescue_from :all do |e|
error_response message: "Internal server error: #{e}", status: 500
internal_server_error!(e)
end

# We are using the same formatter for any error that might be raised. The
# _ignored parameter include (in order): backtrace, options, env and
# original_exception.
error_formatter :json, ->(message, *_ignored) { { errors: message }.to_json }

helpers Pundit
helpers ::API::Helpers

Expand All @@ -60,7 +65,7 @@ class RootAPI < Grape::API
mount ::API::Version

route :any, "*path" do
error!("Not found", 404)
not_found!
end

add_swagger_documentation \
Expand Down
2 changes: 1 addition & 1 deletion lib/api/v1/namespaces.rb
Expand Up @@ -76,7 +76,7 @@ class Namespaces < Grape::API
current_user: current_user,
type: current_type
else
error!({ "errors" => namespace.errors.full_messages }, 422, header)
unprocessable_entity!(namespace.errors.full_messages)
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/api/v1/repositories.rb
Expand Up @@ -121,7 +121,7 @@ class Repositories < Grape::API
destroy_service = ::Repositories::DestroyService.new(current_user)
destroyed = destroy_service.execute(repository)

error!({ "errors" => destroy_service.error }, 422, header) unless destroyed
error!(destroy_service.error, 422, header) unless destroyed
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/api/v1/teams.rb
Expand Up @@ -48,7 +48,7 @@ class Teams < Grape::API
current_user: current_user,
type: current_type
else
error!({ "errors" => team.errors.full_messages }, 422, header)
unprocessable_entity!(team.errors.full_messages)
end
end

Expand Down

0 comments on commit f0432d1

Please sign in to comment.