Commit 970f41cf authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'add-api-shared_runners_minutes_limit' into 'master'

Add shared_runners_minutes_limit to groups and users API

See merge request !1942
parents cba2826e c14bc19d
...@@ -15,6 +15,9 @@ module EE ...@@ -15,6 +15,9 @@ module EE
# column directly. # column directly.
validate :auditor_requires_license_add_on, if: :auditor validate :auditor_requires_license_add_on, if: :auditor
validate :cannot_be_admin_and_auditor validate :cannot_be_admin_and_auditor
delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=,
to: :namespace
end end
module ClassMethods module ClassMethods
......
...@@ -65,7 +65,7 @@ class User < ActiveRecord::Base ...@@ -65,7 +65,7 @@ class User < ActiveRecord::Base
# #
# Namespace for personal projects # Namespace for personal projects
has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, autosave: true
# Profile # Profile
has_many :keys, -> do has_many :keys, -> do
......
---
title: Add shared_runners_minutes_limit to groups and users API
merge_request: 1942
author:
...@@ -140,6 +140,7 @@ Example response: ...@@ -140,6 +140,7 @@ Example response:
"full_name": "Twitter", "full_name": "Twitter",
"full_path": "twitter", "full_path": "twitter",
"parent_id": null, "parent_id": null,
"shared_runners_minutes_limit": 133,
"projects": [ "projects": [
{ {
"id": 7, "id": 7,
...@@ -290,6 +291,7 @@ Parameters: ...@@ -290,6 +291,7 @@ Parameters:
- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group - `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group
- `request_access_enabled` (optional) - Allow users to request member access. - `request_access_enabled` (optional) - Allow users to request member access.
- `parent_id` (optional) - The parent group id for creating nested group. - `parent_id` (optional) - The parent group id for creating nested group.
- `shared_runners_minutes_limit` (optional) - (admin-only) Pipeline minutes quota for this group
## Transfer project to group ## Transfer project to group
...@@ -323,6 +325,7 @@ PUT /groups/:id ...@@ -323,6 +325,7 @@ PUT /groups/:id
| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. | | `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. |
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group | | `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group |
| `request_access_enabled` | boolean | no | Allow users to request member access. | | `request_access_enabled` | boolean | no | Allow users to request member access. |
| `shared_runners_minutes_limit` | integer | no | (admin-only) Pipeline minutes quota for this group |
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/5?name=Experimental" curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/5?name=Experimental"
......
...@@ -220,7 +220,8 @@ Parameters: ...@@ -220,7 +220,8 @@ Parameters:
"can_create_group": true, "can_create_group": true,
"can_create_project": true, "can_create_project": true,
"two_factor_enabled": true, "two_factor_enabled": true,
"external": false "external": false,
"shared_runners_minutes_limit": 133
} }
``` ```
...@@ -253,6 +254,7 @@ Parameters: ...@@ -253,6 +254,7 @@ Parameters:
- `can_create_group` (optional) - User can create groups - true or false - `can_create_group` (optional) - User can create groups - true or false
- `confirm` (optional) - Require confirmation - true (default) or false - `confirm` (optional) - Require confirmation - true (default) or false
- `external` (optional) - Flags the user as external - true or false(default) - `external` (optional) - Flags the user as external - true or false(default)
- `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user
## User modification ## User modification
...@@ -281,6 +283,7 @@ Parameters: ...@@ -281,6 +283,7 @@ Parameters:
- `admin` (optional) - User is admin - true or false (default) - `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false - `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default) - `external` (optional) - Flags the user as external - true or false(default)
- `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user
On password update, user will be forced to change it upon next login. On password update, user will be forced to change it upon next login.
Note, at the moment this method does only return a `404` error, Note, at the moment this method does only return a `404` error,
......
...@@ -41,6 +41,9 @@ module API ...@@ -41,6 +41,9 @@ module API
expose :can_create_project?, as: :can_create_project expose :can_create_project?, as: :can_create_project
expose :two_factor_enabled?, as: :two_factor_enabled expose :two_factor_enabled?, as: :two_factor_enabled
expose :external expose :external
# EE-only
expose :shared_runners_minutes_limit
end end
class UserWithPrivateDetails < UserPublic class UserWithPrivateDetails < UserPublic
...@@ -188,6 +191,9 @@ module API ...@@ -188,6 +191,9 @@ module API
class GroupDetail < Group class GroupDetail < Group
expose :projects, using: Entities::Project expose :projects, using: Entities::Project
expose :shared_projects, using: Entities::Project expose :shared_projects, using: Entities::Project
# EE-only
expose :shared_runners_minutes_limit
end end
class RepoCommit < Grape::Entity class RepoCommit < Grape::Entity
......
...@@ -17,6 +17,7 @@ module API ...@@ -17,6 +17,7 @@ module API
optional :membership_lock, type: Boolean, desc: 'Prevent adding new members to project membership within this group' optional :membership_lock, type: Boolean, desc: 'Prevent adding new members to project membership within this group'
optional :ldap_cn, type: String, desc: 'LDAP Common Name' optional :ldap_cn, type: String, desc: 'LDAP Common Name'
optional :ldap_access, type: Integer, desc: 'A valid access level' optional :ldap_access, type: Integer, desc: 'A valid access level'
optional :shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Pipeline minutes quota for this group'
all_or_none_of :ldap_cn, :ldap_access all_or_none_of :ldap_cn, :ldap_access
end end
...@@ -89,6 +90,9 @@ module API ...@@ -89,6 +90,9 @@ module API
group_access: params.delete(:ldap_access) group_access: params.delete(:ldap_access)
} }
# EE
authenticated_as_admin! if params[:shared_runners_minutes_limit]
group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
if group.persisted? if group.persisted?
...@@ -100,7 +104,7 @@ module API ...@@ -100,7 +104,7 @@ module API
) )
end end
present group, with: Entities::Group, current_user: current_user present group, with: Entities::GroupDetail, current_user: current_user
else else
render_api_error!("Failed to save group #{group.errors.messages}", 400) render_api_error!("Failed to save group #{group.errors.messages}", 400)
end end
...@@ -118,13 +122,18 @@ module API ...@@ -118,13 +122,18 @@ module API
optional :name, type: String, desc: 'The name of the group' optional :name, type: String, desc: 'The name of the group'
optional :path, type: String, desc: 'The path of the group' optional :path, type: String, desc: 'The path of the group'
use :optional_params use :optional_params
at_least_one_of :name, :path, :description, :visibility,
:lfs_enabled, :request_access_enabled
end end
put ':id' do put ':id' do
group = find_group!(params[:id]) group = find_group!(params[:id])
authorize! :admin_group, group authorize! :admin_group, group
# EE
if params[:shared_runners_minutes_limit].present? &&
group.shared_runners_minutes_limit.to_i !=
params[:shared_runners_minutes_limit].to_i
authenticated_as_admin!
end
if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
present group, with: Entities::GroupDetail, current_user: current_user present group, with: Entities::GroupDetail, current_user: current_user
else else
......
...@@ -30,6 +30,9 @@ module API ...@@ -30,6 +30,9 @@ module API
optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed' optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
all_or_none_of :extern_uid, :provider all_or_none_of :extern_uid, :provider
# EE
optional :shared_runners_minutes_limit, type: Integer, desc: 'Pipeline minutes quota for this user'
end end
end end
...@@ -127,10 +130,6 @@ module API ...@@ -127,10 +130,6 @@ module API
optional :name, type: String, desc: 'The name of the user' optional :name, type: String, desc: 'The name of the user'
optional :username, type: String, desc: 'The username of the user' optional :username, type: String, desc: 'The username of the user'
use :optional_attributes use :optional_attributes
at_least_one_of :email, :password, :name, :username, :skype, :linkedin,
:twitter, :website_url, :organization, :projects_limit,
:extern_uid, :provider, :bio, :location, :admin,
:can_create_group, :confirm, :external
end end
put ":id" do put ":id" do
authenticated_as_admin! authenticated_as_admin!
......
...@@ -13,6 +13,14 @@ describe User, models: true do ...@@ -13,6 +13,14 @@ describe User, models: true do
it { is_expected.to include_module(TokenAuthenticatable) } it { is_expected.to include_module(TokenAuthenticatable) }
end end
describe 'delegations' do
it { is_expected.to delegate_method(:path).to(:namespace).with_prefix }
# EE
it { is_expected.to delegate_method(:shared_runners_minutes_limit).to(:namespace) }
it { is_expected.to delegate_method(:shared_runners_minutes_limit=).to(:namespace).with_arguments(133) }
end
describe 'associations' do describe 'associations' do
it { is_expected.to have_one(:namespace) } it { is_expected.to have_one(:namespace) }
it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:snippets).dependent(:destroy) }
......
...@@ -272,6 +272,25 @@ describe API::Groups do ...@@ -272,6 +272,25 @@ describe API::Groups do
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
# EE
it 'returns 403 for updating shared_runners_minutes_limit' do
expect do
put api("/groups/#{group1.id}", user1), shared_runners_minutes_limit: 133
end.not_to change { group1.shared_runners_minutes_limit }
expect(response).to have_http_status(403)
end
it 'returns 200 if shared_runners_minutes_limit is not changing' do
group1.update(shared_runners_minutes_limit: 133)
expect do
put api("/groups/#{group1.id}", user1), shared_runners_minutes_limit: 133
end.not_to change { group1.shared_runners_minutes_limit }
expect(response).to have_http_status(200)
end
end end
context 'when authenticated as the admin' do context 'when authenticated as the admin' do
...@@ -281,6 +300,17 @@ describe API::Groups do ...@@ -281,6 +300,17 @@ describe API::Groups do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['name']).to eq(new_group_name) expect(json_response['name']).to eq(new_group_name)
end end
# EE
it 'updates the group for shared_runners_minutes_limit' do
expect do
put api("/groups/#{group1.id}", admin), shared_runners_minutes_limit: 133
end.to change { group1.reload.shared_runners_minutes_limit }
.from(nil).to(133)
expect(response).to have_http_status(200)
expect(json_response['shared_runners_minutes_limit']).to eq(133)
end
end end
context 'when authenticated as an user that can see the group' do context 'when authenticated as an user that can see the group' do
...@@ -479,23 +509,36 @@ describe API::Groups do ...@@ -479,23 +509,36 @@ describe API::Groups do
group_attributes = attributes_for(:group, ldap_cn: 'ldap-group', ldap_access: Gitlab::Access::DEVELOPER) group_attributes = attributes_for(:group, ldap_cn: 'ldap-group', ldap_access: Gitlab::Access::DEVELOPER)
expect { post api("/groups", admin), group_attributes }.to change{ LdapGroupLink.count }.by(1) expect { post api("/groups", admin), group_attributes }.to change{ LdapGroupLink.count }.by(1)
end end
end
end
describe "PUT /groups" do # EE
context "when authenticated as user without group permissions" do context 'when shared_runners_minutes_limit is given' do
it "does not create group" do context 'when the current user is not an admin' do
put api("/groups/#{group2.id}", user1), attributes_for(:group) it "does not create a group with shared_runners_minutes_limit" do
expect(response.status).to eq(404) group = attributes_for(:group, { shared_runners_minutes_limit: 133 })
end
end
context "when authenticated as user with group permissions" do expect do
it "updates group" do post api("/groups", user3), group
group2.update(owner: user2) end.not_to change { Group.count }
put api("/groups/#{group2.id}", user2), { name: 'Renamed' }
expect(response.status).to eq(200) expect(response).to have_http_status(403)
expect(group2.reload.name).to eq('Renamed') end
end
context 'when the current user is an admin' do
it "creates a group with shared_runners_minutes_limit" do
group = attributes_for(:group, { shared_runners_minutes_limit: 133 })
expect do
post api("/groups", admin), group
end.to change { Group.count }.by(1)
created_group = Group.find(json_response['id'])
expect(created_group.shared_runners_minutes_limit).to eq(133)
expect(response).to have_http_status(201)
expect(json_response['shared_runners_minutes_limit']).to eq(133)
end
end
end end
end end
end end
......
...@@ -425,6 +425,17 @@ describe API::Users do ...@@ -425,6 +425,17 @@ describe API::Users do
expect(user.reload.external?).to be_truthy expect(user.reload.external?).to be_truthy
end end
# EE
it "updates shared_runners_minutes_limit" do
expect do
put api("/users/#{user.id}", admin), { shared_runners_minutes_limit: 133 }
end.to change { user.reload.shared_runners_minutes_limit }
.from(nil).to(133)
expect(response).to have_http_status(200)
expect(json_response['shared_runners_minutes_limit']).to eq(133)
end
it "does not update admin status" do it "does not update admin status" do
put api("/users/#{admin_user.id}", admin), { can_create_group: false } put api("/users/#{admin_user.id}", admin), { can_create_group: false }
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
...@@ -438,9 +449,22 @@ describe API::Users do ...@@ -438,9 +449,22 @@ describe API::Users do
expect(user.reload.email).not_to eq('invalid email') expect(user.reload.email).not_to eq('invalid email')
end end
it "is not available for non admin users" do context 'when the current user is not an admin' do
put api("/users/#{user.id}", user), attributes_for(:user) it "is not available" do
expect(response).to have_http_status(403) expect do
put api("/users/#{user.id}", user), attributes_for(:user)
end.not_to change { user.reload.attributes }
expect(response).to have_http_status(403)
end
it "cannot update their own shared_runners_minutes_limit" do
expect do
put api("/users/#{user.id}", user), { shared_runners_minutes_limit: 133 }
end.not_to change { user.reload.shared_runners_minutes_limit }
expect(response).to have_http_status(403)
end
end end
it "returns 404 for non-existing user" do it "returns 404 for non-existing user" do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment