Skip to content
This repository was archived by the owner on Apr 17, 2023. It is now read-only.

Commit f0432d1

Browse files
committed
api: added a bootstrap endpoint
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 #1437). See #1412 Signed-off-by: Miquel Sabaté Solà <[email protected]>
1 parent 73162e3 commit f0432d1

File tree

11 files changed

+387
-184
lines changed

11 files changed

+387
-184
lines changed

config/config.yml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,18 @@ oauth:
163163
# Only members of team can sign in/up with Bitbucket. Need permission to read team membership.
164164
team: ""
165165

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

lib/api/helpers.rb

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
# frozen_string_literal: true
22

33
require "portus/auth_from_token"
4+
require "api/helpers/errors"
5+
require "api/helpers/namespaces"
46

57
module API
68
module Helpers
79
include ::Portus::AuthFromToken
810

11+
include Errors
12+
include Namespaces
13+
914
# On success it will fill the @user instance variable with the currently
1015
# authenticated user for the API. Otherwise it will raise:
1116
#
@@ -21,7 +26,7 @@ def authorization!(force_admin: true)
2126

2227
current_user
2328

24-
error!("Authentication fails.", 401) unless @user
29+
unauthorized!("Authentication fails") unless @user
2530
raise Pundit::NotAuthorizedError if force_admin && !@user.admin
2631
end
2732

@@ -62,15 +67,5 @@ def permitted_params
6267
include_missing: false,
6368
include_parent_namespaces: false)
6469
end
65-
66-
# Helpers for namespaces
67-
module Namespaces
68-
# Returns an aggregate of the accessible namespaces for the current user.
69-
def accessible_namespaces
70-
special = Namespace.special_for(current_user).order(created_at: :asc)
71-
normal = policy_scope(Namespace).order(created_at: :asc)
72-
special + normal
73-
end
74-
end
7570
end
7671
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
module API
4+
module Helpers
5+
# Helpers regarding the management of authentication tokens. This module
6+
# mostly contains methods that are shared across different paths.
7+
module ApplicationTokens
8+
# Create an application token for the given user with the given
9+
# ID. The `params` parameter contains the parameters to be passed to the
10+
# `ApplicationToken.create_token` method as `params`.
11+
#
12+
# This method already sends the proper HTTP response and code.
13+
def create_application_token!(user, id, params)
14+
if user.valid?
15+
application_token, plain_token = ApplicationToken.create_token(
16+
current_user: user,
17+
user_id: id,
18+
params: params
19+
)
20+
21+
if application_token.errors.empty?
22+
status 201
23+
{ plain_token: plain_token }
24+
else
25+
bad_request!(application_token.errors)
26+
end
27+
else
28+
bad_request!(user.errors)
29+
end
30+
end
31+
end
32+
end
33+
end

lib/api/helpers/errors.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
# frozen_string_literal: true
3+
4+
require "portus/auth_from_token"
5+
6+
module API
7+
module Helpers
8+
# Errors implements helper methods for API error responses.
9+
module Errors
10+
def api_error!(code:, messages:)
11+
obj = messages.is_a?(String) ? [messages] : messages
12+
error!(obj, code)
13+
end
14+
15+
# Sends a `400 Bad Request` error with a possible message as the response
16+
# body.
17+
def bad_request!(msg = "Bad Request")
18+
api_error!(code: 400, messages: msg)
19+
end
20+
21+
# Sends a `401 Unauthorized` error with a possible message as the response
22+
# body.
23+
def unauthorized!(msg = "Unauthorized")
24+
api_error!(code: 401, messages: msg)
25+
end
26+
27+
# Sends a `403 Forbidden` error with a possible message as the response
28+
# body.
29+
def forbidden!(msg = "Forbidden")
30+
api_error!(code: 403, messages: msg)
31+
end
32+
33+
# Sends a `404 Not found` error with a possible message as the response
34+
# body.
35+
def not_found!(msg = "Not found")
36+
api_error!(code: 404, messages: msg)
37+
end
38+
39+
# Sends a `405 Method Not Allowed` error with a possible message as the
40+
# response body.
41+
def method_not_allowed!(msg = "Method Not Allowed")
42+
api_error!(code: 405, messages: msg)
43+
end
44+
45+
# Sends a `422 Unprocessable Entity` error with a possible message as the
46+
# response body.
47+
def unprocessable_entity!(msg = "Unprocessable Entity")
48+
api_error!(code: 422, messages: msg)
49+
end
50+
51+
# Sends a `405 Internal Server Error` error with a possible message as the
52+
# response body.
53+
def internal_server_error!(msg = "Internal Server Error")
54+
api_error!(code: 500, messages: msg)
55+
end
56+
end
57+
end
58+
end

lib/api/helpers/namespaces.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
module API
4+
module Helpers
5+
# Helpers for namespaces
6+
module Namespaces
7+
# Returns an aggregate of the accessible namespaces for the current user.
8+
def accessible_namespaces
9+
special = Namespace.special_for(current_user).order(created_at: :asc)
10+
normal = policy_scope(Namespace).order(created_at: :asc)
11+
special + normal
12+
end
13+
end
14+
end
15+
end

lib/api/root_api.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,31 @@ class RootAPI < Grape::API
2626
end
2727

2828
rescue_from ActiveRecord::RecordNotFound do
29-
error_response message: "Not found", status: 404
29+
not_found!
3030
end
3131

3232
rescue_from Grape::Exceptions::MethodNotAllowed do |e|
33-
error_response message: { errors: e.message }, status: 405
33+
method_not_allowed!(e.message)
3434
end
3535

3636
rescue_from Grape::Exceptions::ValidationErrors do |e|
37-
error_response message: { errors: e.errors }, status: 400
37+
bad_request!(e.errors)
3838
end
3939

4040
rescue_from Pundit::NotAuthorizedError do |_|
41-
error_response message: { errors: "Authorization fails" }, status: 403
41+
forbidden!("Authorization fails")
4242
end
4343

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

49+
# We are using the same formatter for any error that might be raised. The
50+
# _ignored parameter include (in order): backtrace, options, env and
51+
# original_exception.
52+
error_formatter :json, ->(message, *_ignored) { { errors: message }.to_json }
53+
4954
helpers Pundit
5055
helpers ::API::Helpers
5156

@@ -60,7 +65,7 @@ class RootAPI < Grape::API
6065
mount ::API::Version
6166

6267
route :any, "*path" do
63-
error!("Not found", 404)
68+
not_found!
6469
end
6570

6671
add_swagger_documentation \

lib/api/v1/namespaces.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class Namespaces < Grape::API
7676
current_user: current_user,
7777
type: current_type
7878
else
79-
error!({ "errors" => namespace.errors.full_messages }, 422, header)
79+
unprocessable_entity!(namespace.errors.full_messages)
8080
end
8181
end
8282

lib/api/v1/repositories.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class Repositories < Grape::API
121121
destroy_service = ::Repositories::DestroyService.new(current_user)
122122
destroyed = destroy_service.execute(repository)
123123

124-
error!({ "errors" => destroy_service.error }, 422, header) unless destroyed
124+
error!(destroy_service.error, 422, header) unless destroyed
125125
end
126126
end
127127
end

lib/api/v1/teams.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class Teams < Grape::API
4848
current_user: current_user,
4949
type: current_type
5050
else
51-
error!({ "errors" => team.errors.full_messages }, 422, header)
51+
unprocessable_entity!(team.errors.full_messages)
5252
end
5353
end
5454

0 commit comments

Comments
 (0)