Commit 1c2e67ff authored by Sean McGivern's avatar Sean McGivern

Merge branch '5488_license_management_app_blacklist_backend' into 'master'

Add License Management API to the backend

Closes #5488

See merge request gitlab-org/gitlab-ee!6246
parents 63e4129d 68a087e7
......@@ -519,6 +519,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# its preferable to keep it below all other project routes
draw :wiki
draw :repository
## EE-specific
resources :managed_licenses, only: [:index, :show, :new, :create, :edit, :update, :destroy]
## EE-specific
end
resources(:projects,
......
......@@ -2463,6 +2463,20 @@ ActiveRecord::Schema.define(version: 20180722103201) do
add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
create_table "software_license_policies", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "software_license_id", null: false
t.integer "approval_status", default: 0, null: false
end
add_index "software_license_policies", ["project_id", "software_license_id"], name: "index_software_license_policies_unique_per_project", unique: true, using: :btree
create_table "software_licenses", force: :cascade do |t|
t.string "name", null: false
end
add_index "software_licenses", ["name"], name: "index_software_licenses_on_name", using: :btree
create_table "spam_logs", force: :cascade do |t|
t.integer "user_id"
t.string "source_ip"
......@@ -3021,6 +3035,8 @@ ActiveRecord::Schema.define(version: 20180722103201) do
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
add_foreign_key "software_license_policies", "projects", on_delete: :cascade
add_foreign_key "software_license_policies", "software_licenses", on_delete: :cascade
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
add_foreign_key "term_agreements", "application_setting_terms", column: "term_id"
......
......@@ -38,6 +38,7 @@ following locations:
- [Keys](keys.md)
- [Labels](labels.md)
- [License](license.md)
- [Managed licenses](managed_licenses.md) **[ULTIMATE]**
- [Markdown](markdown.md)
- [Merge Requests](merge_requests.md)
- [Merge Request Approvals](merge_request_approvals.md) **[STARTER]**
......
# Managed Licenses API **[ULTIMATE]**
## List managed licenses
Get all managed licenses for a given project.
```
GET /projects/:id/managed_licenses
```
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/managed_licenses
```
Example response:
```json
[
{
"id": 1,
"name": "MIT",
"approval_status": "approved"
},
{
"id": 3,
"name": "ISC",
"approval_status": "blacklisted"
}
]
```
## Show an existing managed license
Shows an existing managed license.
```
GET /projects/:id/managed_licenses/:managed_license_id
```
| Attribute | Type | Required | Description |
| --------------- | ------- | --------------------------------- | ------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/6"
```
Example response:
```json
{
"id": 1,
"name": "MIT",
"approval_status": "blacklisted"
}
```
## Create a new managed license
Creates a new managed license for the given project with the given name and approval status.
```
POST /projects/:id/managed_licenses
```
| Attribute | Type | Required | Description |
| ------------- | ------- | -------- | ---------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the managed license |
| `approval_status` | string | yes | The approval status. "approved" or "blacklisted" |
```bash
curl --data "name=MIT&approval_status=blacklisted" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/managed_licenses"
```
Example response:
```json
{
"id": 1,
"name": "MIT",
"approval_status": "approved"
}
```
## Delete a managed license
Deletes a managed license with a given id.
```
DELETE /projects/:id/managed_licenses/:managed_license_id
```
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/4"
```
When successful, it replies with an HTTP 204 response.
## Edit an existing managed license
Updates an existing managed license with a new approval status.
```
PATCH /projects/:id/managed_licenses/:managed_license_id
```
| Attribute | Type | Required | Description |
| --------------- | ------- | --------------------------------- | ------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project |
| `approval_status` | string | yes | The approval status. "approved" or "blacklisted" |
```bash
curl --request PATCH --data "approval_status=blacklisted" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/6"
```
Example response:
```json
{
"id": 1,
"name": "MIT",
"approval_status": "blacklisted"
}
```
# frozen_string_literal: true
class Projects::ManagedLicensesController < Projects::ApplicationController
before_action :software_license_policy, only: [:show, :edit, :update, :destroy]
before_action :authorize_can_read!, only: [:index, :show]
before_action :authorize_can_admin!, only: [:new, :create, :edit, :update, :destroy]
def index
respond_to do |format|
format.json do
render_software_license_policies
end
end
end
def show
respond_to do |format|
format.json do
render_software_license_policy
end
end
end
def new
@software_license_policy = @project.software_license_policies.new
end
def create
result = SoftwareLicensePolicies::CreateService.new(
@project,
current_user,
software_license_policies_params
).execute
if result[:status] == :success
@software_license_policy = result[:software_license_policy]
respond_to do |format|
format.json { render_software_license_policy }
end
else
respond_to do |format|
format.json { render_error(result[:message], 400) }
end
end
end
def edit
end
def update
result = SoftwareLicensePolicies::UpdateService.new(
@project,
current_user,
software_license_policies_params
).execute(@software_license_policy)
if result[:status] == :success
respond_to do |format|
format.json { render_software_license_policy }
end
else
respond_to do |format|
format.json { render_error(result[:message], 400) }
end
end
end
def destroy
@software_license_policy.destroy!
respond_to do |format|
format.json { render_ok }
end
end
private
def respond_400
head :bad_request
end
# Fetch the existing software license policy when given an id or name
def software_license_policy
id = params[:id]
id = CGI.unescape(id) unless id.is_a?(Integer) || id =~ /^\d+$/
@software_license_policy ||= SoftwareLicensePoliciesFinder.new(current_user, project).find_by_name_or_id(id)
if @software_license_policy.nil?
# The license was not found
render_404
end
end
def render_ok
render status: :ok, nothing: true
end
def render_software_license_policy
render status: :ok, json: ManagedLicenseSerializer.new.represent(@software_license_policy)
end
def render_software_license_policies
render status: :ok, json: { software_license_policies: ManagedLicenseSerializer.new.represent(@project.software_license_policies) }
end
def render_error(error, status = 400)
render json: error, status: status
end
def software_license_policies_params
# Require the presence of an hash containing the software license policy fields
params.require(:managed_license).permit(:name, :approval_status)
end
def authorize_can_read!
render_404 unless can?(current_user, :read_software_license_policy, @project)
end
def authorize_can_admin!
authorize_can_read!
render_403 unless can?(current_user, :admin_software_license_policy, @project)
end
end
# frozen_string_literal: true
class SoftwareLicensePoliciesFinder
include Gitlab::Allowable
include FinderMethods
attr_accessor :current_user, :project
def initialize(current_user, project)
@current_user = current_user
@project = project
end
def find_by_name_or_id(id)
return nil unless can?(current_user, :read_software_license_policy, project)
software_licenses = SoftwareLicense.arel_table
software_license_policies = SoftwareLicensePolicy.arel_table
project.software_license_policies.joins(:software_license).where(
software_licenses[:name].eq(id).or(software_license_policies[:id].eq(id))
).take
end
end
......@@ -34,6 +34,8 @@ module EE
has_many :audit_events, as: :entity
has_many :path_locks
has_many :vulnerability_feedback
has_many :software_license_policies, inverse_of: :project, class_name: 'SoftwareLicensePolicy'
accepts_nested_attributes_for :software_license_policies, allow_destroy: true
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
......
# frozen_string_literal: true
# This class represents a software license.
# For use in the License Management feature.
class SoftwareLicense < ActiveRecord::Base
include Presentable
validates :name, presence: true
scope :ordered, -> { order(:name) }
end
# frozen_string_literal: true
# This class represents a software license policy. Which means the fact that the user
# approves or not of the use of a certain software license in their project.
# For use in the License Management feature.
class SoftwareLicensePolicy < ActiveRecord::Base
include Presentable
# Only allows modification of the approval status
FORM_EDITABLE = %i[approval_status].freeze
belongs_to :project, inverse_of: :software_license_policies
belongs_to :software_license, -> { readonly }
attr_readonly :software_license
# Licenses must be approved or blacklisted.
enum approval_status: {
blacklisted: 0,
approved: 1
}
# Software license is mandatory, it contains the license informations.
validates_associated :software_license
validates_presence_of :software_license
validates_presence_of :project
validates :approval_status, presence: true
# A license is unique for its project since it can't be approved and blacklisted.
validates :software_license, uniqueness: { scope: :project_id }
scope :ordered, -> { SoftwareLicensePolicy.includes(:software_license).order("software_licenses.name ASC") }
def name
software_license.name
end
end
......@@ -7,6 +7,7 @@ module EE
issue_link
approvers
vulnerability_feedback
license_management
].freeze
prepended do
......@@ -53,6 +54,11 @@ module EE
@subject.feature_available?(:prometheus_alerts, @user)
end
with_scope :subject
condition(:license_management_enabled) do
@subject.feature_available?(:license_management)
end
rule { admin }.enable :change_repository_storage
rule { support_bot }.enable :guest_access
......@@ -90,6 +96,8 @@ module EE
rule { can?(:read_project) }.enable :read_vulnerability_feedback
rule { license_management_enabled & can?(:read_project) }.enable :read_software_license_policy
rule { repository_mirrors_enabled & ((mirror_available & can?(:admin_project)) | admin) }.enable :admin_mirror
rule { deploy_board_disabled & ~is_development }.prevent :read_deploy_board
......@@ -100,6 +108,8 @@ module EE
enable :update_approvers
end
rule { license_management_enabled & can?(:maintainer_access) }.enable :admin_software_license_policy
rule { pod_logs_enabled & can?(:maintainer_access) }.enable :read_pod_logs
rule { prometheus_alerts_enabled & can?(:maintainer_access) }.enable :read_prometheus_alerts
......
module EE
module MergeRequestWidgetEntity
include ::API::Helpers::RelatedResourcesHelpers
extend ActiveSupport::Concern
prepended do
......@@ -99,6 +100,14 @@ module EE
merge_request.base_license_management_artifact,
path: Ci::Build::LICENSE_MANAGEMENT_FILE)
end
expose :managed_licenses_path do |merge_request|
api_v4_projects_managed_licenses_path(id: merge_request.source_project.id)
end
expose :can_manage_licenses do |merge_request|
can?(current_user, :admin_software_license_policy, merge_request)
end
end
# expose_sast_container_data? is deprecated and replaced with expose_container_scanning_data? (#5778)
......
# frozen_string_literal: true
class ManagedLicenseEntity < Grape::Entity
expose :id
expose :approval_status
expose :software_license, merge: true do
expose :name
end
end
# frozen_string_literal: true
class ManagedLicenseSerializer < BaseSerializer
entity ManagedLicenseEntity
end
# frozen_string_literal: true
# Managed license creation service. For use in the managed license controller.
module SoftwareLicensePolicies
class CreateService < ::BaseService
def initialize(project, user, params)
super(project, user, params.with_indifferent_access)
end
# Returns the created managed license.
def execute
return error("", 403) unless can?(@current_user, :admin_software_license_policy, @project)
# Load or create the software license
name = params.delete(:name)
software_license = SoftwareLicense.transaction do
begin
SoftwareLicense.transaction(requires_new: true) do
SoftwareLicense.find_or_create_by(name: name)
end
rescue ActiveRecord::RecordNotUnique
retry
end
end
# Add the software license to params
params[:software_license] = software_license
begin
software_license_policy = @project.software_license_policies.create(params)
rescue ArgumentError => ex
return error(ex.message, 400)
end
if software_license_policy.errors.any?
return error(software_license_policy.errors.full_messages.join("\n"), 400)
end
success(software_license_policy: software_license_policy)
end
end
end
# frozen_string_literal: true
# Managed license update service. For use in the managed license controller.
module SoftwareLicensePolicies
class UpdateService < ::BaseService
def initialize(project, user, params)
super(project, user, params.with_indifferent_access)
end
# returns the updated managed license
def execute(software_license_policy)
return error("", 403) unless can?(@current_user, :admin_software_license_policy, @project)
@params = @params.slice(*SoftwareLicensePolicy::FORM_EDITABLE)
begin
software_license_policy.update(params)
rescue ArgumentError => ex
return error(ex.message, 400)
end
success(software_license_policy: software_license_policy)
end
end
end
---
title: Add an API endpoint for managed licenses of a project.
merge_request: 6246
author:
type: added
# frozen_string_literal: true
# A software license. Used in the License Management feature for CI/CD.
class CreateSoftwareLicenses < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
create_table :software_licenses do |t|
t.string :name, null: false, unique: true, index: true
end
end
end
# frozen_string_literal: true
class CreateSoftwareLicensePolicies < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_software_license_policies_unique_per_project'
disable_ddl_transaction!
def up
create_table :software_license_policies do |t|
t.references :project, index: false, foreign_key: { on_delete: :cascade }, null: false
t.references :software_license, index: false, foreign_key: { on_delete: :cascade }, null: false
t.integer :approval_status, null: false, default: 0 # Defaults to blacklisted
end
add_concurrent_index :software_license_policies,
[:project_id, :software_license_id],
unique: true,
name: INDEX_NAME
end
def down
if foreign_keys_for(:software_license_policies, :project_id).any?
remove_foreign_key :software_license_policies, column: :project_id
end
if foreign_keys_for(:software_license_policies, :software_license_id).any?
remove_foreign_key :software_license_policies, column: :software_license_id
end
if index_exists?(:software_license_policies, [:project_id, :software_license_id])
remove_concurrent_index :software_license_policies, [:project_id, :software_license_id]
end
if table_exists?(:software_license_policies)
drop_table :software_license_policies
end
end
end
# frozen_string_literal: true
module API
class ManagedLicenses < Grape::API
include PaginationParams
before { authenticate! }
helpers do
# Make the software license policy specified by id in the request available
def software_license_policy
id = params[:managed_license_id]
@software_license_policy ||= SoftwareLicensePoliciesFinder.new(current_user, user_project).find_by_name_or_id(id)
end
def authorize_can_read!
authorize!(:read_software_license_policy, user_project)
end
def authorize_can_admin!
authorize!(:admin_software_license_policy, user_project)
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get project software license policies' do
success Entities::ManagedLicense
end
params do
use :pagination
end
get ':id/managed_licenses' do
authorize_can_read!
software_license_policies = user_project.software_license_policies
present paginate(software_license_policies), with: Entities::ManagedLicense
end
desc 'Get a specific software license policy from a project' do
success Entities::ManagedLicense
end
get ':id/managed_licenses/:managed_license_id', requirements: { managed_license_id: /.*/ } do
authorize_can_read!
break not_found!('SoftwareLicensePolicy') unless software_license_policy
present software_license_policy, with: Entities::ManagedLicense
end
desc 'Create a new software license policy in a project' do
success Entities::ManagedLicense
end
params do
requires :name, type: String, desc: 'The name of the license'
requires :approval_status,
type: String,
values: %w(approved blacklisted),
desc: 'The approval status of the license. "blacklisted" or "approved".'
end
post ':id/managed_licenses' do
authorize_can_admin!
result = SoftwareLicensePolicies::CreateService.new(
user_project,
current_user,
declared_params(include_missing: false)
).execute
created_software_license_policy = result[:software_license_policy]
if result[:status] == :success
present created_software_license_policy, with: Entities::ManagedLicense
else
render_api_error!(result[:message], result[:http_status])
end
end
desc 'Update an existing software license policy from a project' do
success Entities::ManagedLicense
end
params do
optional :name, type: String, desc: 'The name of the license'
optional :approval_status,
type: String,
values: %w(approved blacklisted),
desc: 'The approval status of the license. "blacklisted" or "approved".'
end
patch ':id/managed_licenses/:managed_license_id', requirements: { managed_license_id: /.*/ } do
authorize_can_admin!
break not_found!('SoftwareLicensePolicy') unless software_license_policy
result = SoftwareLicensePolicies::UpdateService.new(
user_project,
current_user,
declared_params(include_missing: false).except(:id, :name)
).execute(@software_license_policy)
if result[:status] == :success
present @software_license_policy, with: Entities::ManagedLicense
else
render_api_error!(result[:message], result[:http_status])
end
end
desc 'Delete an existing software license policy from a project' do
success Entities::ManagedLicense
end
delete ':id/managed_licenses/:managed_license_id', requirements: { managed_license_id: /.*/ } do
authorize_can_admin!
not_found!('SoftwareLicensePolicy') unless software_license_policy
status 204
software_license_policy.destroy!
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::ManagedLicensesController do
let(:project) do
create(:project).tap do |p|
@software_license_policy = create(:software_license_policy, project: p)
end
end
let(:maintainer_user) do
create(:user).tap do |u|
project.add_maintainer(u)
end
end
let(:dev_user) do
create(:user).tap do |u|
project.add_developer(u)
end
end
let(:reporter_user) do
create(:user).tap do |u|
create(:project_member, :reporter, user: u, project: project)
end
end
let(:other_user) { create(:user) }
let(:unlogged_user) { nil }
let(:software_license_policy) do
@software_license_policy ||= create(:software_license_policy, project: project)
end
before do
stub_licensed_features(license_management: true)
end
describe 'GET #index' do
subject do
allow(controller).to receive(:current_user).and_return(user)
get :index, namespace_id: project.namespace.to_param, project_id: project, format: :json
end
context 'with license management not available' do
before do
stub_licensed_features(license_management: false)
end
let(:user) { dev_user }
it 'returns a not found status' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with a user without read permission' do
let(:user) { other_user }
it 'returns a not found status' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with no logged in user' do
let(:user) { unlogged_user }
it 'returns an unauthorized status' do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'with a user with read permissions' do
let(:user) { dev_user }
it 'renders the software license policies as json' do
subject
expect(response).to match_response_schema('software_license_policies', dir: 'ee')
end
it 'has only one software license policy' do
subject
expect(json_response['software_license_policies'].count).to eq(1)
end
end
end
describe 'GET #show' do
subject do
allow(controller).to receive(:current_user).and_return(user)
get :show,
namespace_id: project.namespace.to_param,
project_id: project,
id: software_license_policy.id,
format: :json
end
context 'with a user without read permission' do
let(:user) { other_user }
it 'returns a not found status' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with no logged in user' do
let(:user) { unlogged_user }
it 'returns an unauthorized status' do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'with a user with read permissions' do
let(:user) { dev_user }
it 'renders the software license policy as json' do
subject
expect(response).to match_response_schema('software_license_policy', dir: 'ee')
end
it 'has correct values for its fields' do
subject
expect(json_response['name']).to eq(software_license_policy.name)
expect(json_response['approval_status']).to eq(software_license_policy.approval_status)
end
end
end
describe 'GET #show with license name as identifier' do
let(:user) { dev_user }
subject do
allow(controller).to receive(:current_user).and_return(user)
get :show,
namespace_id: project.namespace.to_param,
project_id: project,
id: CGI.escape(software_license_policy.name),
format: :json
end
it 'renders the software license policy as json' do
subject
expect(response).to match_response_schema('software_license_policy', dir: 'ee')
end
it 'has correct values for its fields' do
subject
expect(json_response['name']).to eq(software_license_policy.name)
expect(json_response['approval_status']).to eq(software_license_policy.approval_status)
end
end
describe 'POST #create' do
let(:software_license_policy_attributes) do
{ id: software_license_policy.id,
name: software_license_policy.name,
approval_status: software_license_policy.approval_status }
end
let(:new_software_license_policy_attributes) do
{ name: 'new_name',
approval_status: 'blacklisted' }
end
subject do
allow(controller).to receive(:current_user).and_return(user)
post :create,
namespace_id: project.namespace.to_param,
project_id: project,
managed_license: to_create_software_license_policy_attributes,
format: :json
end
context 'with a user without admin permission' do
let(:user) { dev_user }
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'returns a forbidden status' do
expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'with a user without read permission' do
let(:user) { other_user }
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'returns a not found status' do
expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with no logged in user' do
let(:user) { unlogged_user }
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'returns an unauthorized status' do
expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'with a user with admin permissions' do
let(:user) { maintainer_user }
context 'with duplicate new software license policy parameters' do
let(:to_create_software_license_policy_attributes) do
software_license_policy_attributes.merge(approval_status: 'blacklisted')
end
it 'does not update the existing software license policy' do
expect { subject }.not_to change { software_license_policy.reload.approval_status }
end
it 'does not create the new software license policy' do
expect { subject }.not_to change { project.software_license_policies.count }
end
it 'returns a bad request status' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'with new software license policy parameters' do
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'creates the new software license policy' do
expect { subject }.to change { project.software_license_policies.count }.by(1)
end
it 'returns an ok status' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
describe 'PATCH #update' do
let(:software_license_policy_attributes) do
{ id: software_license_policy.id,
name: software_license_policy.name,
approval_status: software_license_policy.approval_status }
end
let(:new_software_license_policy_attributes) do
{ name: 'new_name',
approval_status: 'blacklisted' }
end
let(:modified_software_license_policy_attributes) do
software_license_policy_attributes.merge(approval_status: 'blacklisted')
end
subject do
allow(controller).to receive(:current_user).and_return(user)
patch :update,
namespace_id: project.namespace.to_param,
project_id: project,
id: software_license_policy.id,
managed_license: modified_software_license_policy_attributes,
format: :json
end
context 'with a user without admin permission' do
let(:user) { dev_user }
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'returns a forbidden status' do
expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'with a user without read permission' do
let(:user) { other_user }
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'returns a not found status' do
expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with no logged in user' do
let(:user) { unlogged_user }
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'returns an unauthorized status' do
expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'with a user with admin permissions' do
let(:user) { maintainer_user }
context 'with invalid new software license policy parameters' do
let(:modified_software_license_policy_attributes) do
software_license_policy_attributes.merge(approval_status: 3)
end
it 'does not update the existing software license policy' do
expect { subject }.not_to change { software_license_policy.reload.approval_status }
end
it 'does not create the new software license policy' do
expect { subject }.not_to change { project.software_license_policies.count }
end
it 'returns a bad request status' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'with valid updated software license policy parameters' do
it 'updates the existing software license policy' do
expect { subject }.to change { software_license_policy.reload.approval_status }.to('blacklisted')
end
it 'does not create a new software license policy' do
expect { subject }.not_to change { project.software_license_policies.count }
end
it 'returns an ok status' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'has the updated software license policies in response' do
subject
expect(response).to match_response_schema('software_license_policy', dir: 'ee')
expect(JSON.parse(response.body)).to eq(modified_software_license_policy_attributes.with_indifferent_access)
end
end
end
end
describe 'DELETE #destroy' do
let(:id_to_destroy) { software_license_policy.id }
subject do
allow(controller).to receive(:current_user).and_return(user)
delete :destroy,
namespace_id: project.namespace.to_param,
project_id: project,
id: id_to_destroy,
format: :json
end
context 'with a user without admin permission' do
let(:user) { dev_user }
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'returns a forbidden status' do
expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'with a user without read permission' do
let(:user) { other_user }
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'returns a not found status' do
expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with no logged in user' do
let(:user) { unlogged_user }
let(:to_create_software_license_policy_attributes) do
new_software_license_policy_attributes
end
it 'returns an unauthorized status' do
expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'with a user with admin permissions' do
let(:user) { maintainer_user }
context 'with an existing software license policy' do
it 'destroys the software license policy' do
expect { subject }.to change { project.software_license_policies.count }.by(-1)
expect { software_license_policy.reload }.to raise_error ActiveRecord::RecordNotFound
end
it 'returns an ok status' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'has an empty response body' do
subject
expect(response.body).to eq("")
end
end
context 'with an unknown software license policy' do
let(:id_to_destroy) { 12341234 }
it 'does not destroy any software license policy' do
expect { subject }.not_to change { project.software_license_policies.count }
expect { software_license_policy.reload }.not_to raise_error
end
it 'returns a not found status' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :software_license, class: SoftwareLicense do
sequence(:name) { |n| "SOFTWARE-LICENSE-2.7/example_#{n}" }
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :software_license_policy, class: SoftwareLicensePolicy do
approval_status 1
project
software_license
end
end
# frozen_string_literal: true
require 'spec_helper'
describe SoftwareLicensePoliciesFinder do
let(:project) { create(:project) }
let(:software_license_policy) { create(:software_license_policy, project: project) }
let(:user) do
create(:user).tap do |u|
project.add_maintainer(u)
end
end
let(:finder) { described_class.new(user, project) }
before do
stub_licensed_features(license_management: true)
end
it 'finds the software license policy' do
expect(finder.find_by_name_or_id(software_license_policy.name)).to eq(software_license_policy)
end
end
{
"type": "object",
"required": ["software_license_policies"],
"properties": {
"software_license_policies": {
"type": "array",
"items": { "$ref": "software_license_policy.json" }
}
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"id",
"name",
"approval_status"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"approval_status": { "type": "string" }
},
"additionalProperties": false
}
# frozen_string_literal: true
require 'spec_helper'
describe SoftwareLicensePolicy do
subject { build(:software_license_policy) }
describe 'validations' do
it { is_expected.to include_module(Presentable) }
it { is_expected.to validate_presence_of(:software_license) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:approval_status) }
it { is_expected.to validate_uniqueness_of(:software_license).scoped_to(:project_id).with_message(/has already been taken/) }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe SoftwareLicense do
subject { build(:software_license) }
describe 'validations' do
it { is_expected.to include_module(Presentable) }
it { is_expected.to validate_presence_of(:name) }
end
end
......@@ -355,4 +355,136 @@ describe ProjectPolicy do
it { is_expected.to be_disallowed(:read_project_security_dashboard) }
end
end
describe 'admin_license_management' do
before do
stub_licensed_features(license_management: true)
end
subject { described_class.new(current_user, project) }
context 'without license management feature available' do
before do
stub_licensed_features(license_management: false)
end
let(:current_user) { admin }
it { is_expected.to be_disallowed(:admin_software_license_policy) }
end
context 'with admin' do
let(:current_user) { admin }
it { is_expected.to be_allowed(:admin_software_license_policy) }
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:admin_software_license_policy) }
end
context 'with maintainer' do
let(:current_user) { maintainer }
it { is_expected.to be_allowed(:admin_software_license_policy) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:admin_software_license_policy) }
end
context 'with reporter' do
let(:current_user) { reporter }
it { is_expected.to be_disallowed(:admin_software_license_policy) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:admin_software_license_policy) }
end
context 'with non member' do
let(:current_user) { create(:user) }
it { is_expected.to be_disallowed(:admin_software_license_policy) }
end
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:admin_software_license_policy) }
end
end
describe 'read_license_management' do
before do
stub_licensed_features(license_management: true)
end
subject { described_class.new(current_user, project) }
context 'without license management feature available' do
before do
stub_licensed_features(license_management: false)
end
let(:current_user) { admin }
it { is_expected.to be_disallowed(:read_software_license_policy) }
end
context 'with admin' do
let(:current_user) { admin }
it { is_expected.to be_allowed(:read_software_license_policy) }
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:read_software_license_policy) }
end
context 'with maintainer' do
let(:current_user) { maintainer }
it { is_expected.to be_allowed(:read_software_license_policy) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_allowed(:read_software_license_policy) }
end
context 'with reporter' do
let(:current_user) { reporter }
it { is_expected.to be_allowed(:read_software_license_policy) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_allowed(:read_software_license_policy) }
end
context 'with non member' do
let(:current_user) { create(:user) }
it { is_expected.to be_allowed(:read_software_license_policy) }
end
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_allowed(:read_software_license_policy) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::ManagedLicenses do
let(:project) do
create(:project).tap do |p|
@software_license_policy = create(:software_license_policy, project: p)
end
end
let(:maintainer_user) do
create(:user).tap do |u|
project.add_maintainer(u)
end
end
let(:dev_user) do
create(:user).tap do |u|
project.add_developer(u)
end
end
let(:reporter_user) do
create(:user).tap do |u|
create(:project_member, :reporter, user: u, project: project)
end
end
let(:software_license_policy) do
@software_license_policy ||= create(:software_license_policy, project: project)
end
before do
stub_licensed_features(license_management: true)
end
describe 'GET /projects/:id/managed_licenses' do
context 'with license management not available' do
before do
stub_licensed_features(license_management: false)
end
it 'returns a forbidden status' do
get api("/projects/#{project.id}/managed_licenses", dev_user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'authorized user with proper permissions' do
it 'returns project managed licenses' do
get api("/projects/#{project.id}/managed_licenses", dev_user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a(Array)
expect(json_response.first['id']).to eq(software_license_policy.id)
expect(json_response.first['name']).to eq(software_license_policy.name)
expect(json_response.first['approval_status']).to eq(software_license_policy.approval_status)
end
end
context 'authorized user without read permissions' do
it 'returns project managed licenses to users with read permissions' do
get api("/projects/#{project.id}/managed_licenses", reporter_user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a(Array)
expect(json_response.first['id']).to eq(software_license_policy.id)
expect(json_response.first['name']).to eq(software_license_policy.name)
expect(json_response.first['approval_status']).to eq(software_license_policy.approval_status)
end
end
context 'unauthorized user' do
it 'does not return project managed licenses' do
get api("/projects/#{project.id}/managed_licenses")
expect(response).to have_gitlab_http_status(401)
end
end
end
describe 'GET /projects/:id/managed_licenses/:managed_license_id' do
context 'authorized user with proper permissions' do
it 'returns project managed license details' do
get api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", dev_user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(software_license_policy.id)
expect(json_response['name']).to eq(software_license_policy.name)
expect(json_response['approval_status']).to eq(software_license_policy.approval_status)
end
it 'returns project managed license details using the license name as key' do
escaped_name = CGI.escape(software_license_policy.name)
get api("/projects/#{project.id}/managed_licenses/#{escaped_name}", dev_user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(software_license_policy.id)
expect(json_response['name']).to eq(software_license_policy.name)
expect(json_response['approval_status']).to eq(software_license_policy.approval_status)
end
it 'responds with 404 Not Found if requesting non-existing managed license' do
get api("/projects/#{project.id}/managed_licenses/1234512345", dev_user)
expect(response).to have_gitlab_http_status(404)
end
end
context 'authorized user with read permissions' do
it 'returns project managed license details' do
get api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", reporter_user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(software_license_policy.id)
expect(json_response['name']).to eq(software_license_policy.name)
expect(json_response['approval_status']).to eq(software_license_policy.approval_status)
end
end
context 'unauthorized user' do
it 'does not return project managed license details' do
get api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}")
expect(response).to have_gitlab_http_status(401)
end
end
end
describe 'POST /projects/:id/managed_licenses' do
context 'authorized user with proper permissions' do
it 'creates managed license' do
expect do
post api("/projects/#{project.id}/managed_licenses", maintainer_user),
name: 'NEW_LICENSE_NAME',
approval_status: 'approved'
end.to change {project.software_license_policies.count}.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response).to have_key('id')
expect(json_response['name']).to eq('NEW_LICENSE_NAME')
expect(json_response['approval_status']).to eq('approved')
end
it 'does not allow to duplicate managed license name' do
expect do
post api("/projects/#{project.id}/managed_licenses", maintainer_user),
name: software_license_policy.name,
approval_status: 'blacklisted'
end.not_to change {project.software_license_policies.count}
expect(response).to have_gitlab_http_status(400)
end
end
context 'authorized user with read permissions' do
it 'does not create managed license' do
post api("/projects/#{project.id}/managed_licenses", dev_user),
name: 'NEW_LICENSE_NAME',
approval_status: 'approved'
expect(response).to have_gitlab_http_status(403)
end
end
context 'authorized user without permissions' do
it 'does not create managed license' do
post api("/projects/#{project.id}/managed_licenses", reporter_user),
name: 'NEW_LICENSE_NAME',
approval_status: 'approved'
expect(response).to have_gitlab_http_status(403)
end
end
context 'unauthorized user' do
it 'does not create managed license' do
post api("/projects/#{project.id}/managed_licenses"),
name: 'NEW_LICENSE_NAME',
approval_status: 'approved'
expect(response).to have_gitlab_http_status(401)
end
end
end
describe 'PATCH /projects/:id/managed_licenses/:managed_license_id' do
context 'authorized user with proper permissions' do
it 'updates managed license data' do
initial_license = project.software_license_policies.first
initial_id = initial_license.id
initial_name = initial_license.name
initial_approval_status = initial_license.approval_status
patch api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", maintainer_user),
approval_status: 'blacklisted'
updated_software_license_policy = project.software_license_policies.reload.first
expect(response).to have_gitlab_http_status(200)
# Check that response is equal to the updated object
expect(json_response['id']).to eq(initial_id)
expect(json_response['name']).to eq(updated_software_license_policy.name)
expect(json_response['approval_status']).to eq(updated_software_license_policy.approval_status)
# Check that the approval status was updated
expect(updated_software_license_policy.approval_status).to eq('blacklisted')
# Check that response is equal to the old object except for the approval status
expect(initial_id).to eq(updated_software_license_policy.id)
expect(initial_name).to eq(updated_software_license_policy.name)
expect(initial_approval_status).not_to eq(updated_software_license_policy.approval_status)
end
it 'responds with 404 Not Found if requesting non-existing managed license' do
patch api("/projects/#{project.id}/managed_licenses/1234512345", maintainer_user)
expect(response).to have_gitlab_http_status(404)
end
end
context 'authorized user with read permissions' do
it 'does not update managed license' do
patch api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", dev_user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'authorized user without permissions' do
it 'does not update managed license' do
patch api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", reporter_user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'unauthorized user' do
it 'does not update managed license' do
patch api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}")
expect(response).to have_gitlab_http_status(401)
end
end
end
describe 'DELETE /projects/:id/managed_licenses/:managed_license_id' do
context 'authorized user with proper permissions' do
it 'deletes managed license' do
expect do
delete api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", maintainer_user)
expect(response).to have_gitlab_http_status(204)
end.to change {project.software_license_policies.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing managed license' do
expect do
delete api("/projects/#{project.id}/managed_licenses/1234512345", maintainer_user)
expect(response).to have_gitlab_http_status(404)
end.not_to change {project.software_license_policies.count}
end
end
context 'authorized user with read permissions' do
it 'does not delete managed license' do
expect do
delete api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", dev_user)
expect(response).to have_gitlab_http_status(403)
end.not_to change {project.software_license_policies.count}
end
end
context 'authorized user without permissions' do
it 'does not delete managed license' do
expect do
delete api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", reporter_user)
expect(response).to have_gitlab_http_status(403)
end.not_to change {project.software_license_policies.count}
end
end
context 'unauthorized user' do
it 'does not delete managed license' do
expect do
delete api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}")
expect(response).to have_gitlab_http_status(401)
end.not_to change {project.software_license_policies.count}
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ManagedLicenseEntity do
let(:software_license_policy) { create(:software_license_policy) }
let(:entity) { described_class.new(software_license_policy) }
describe '#as_json' do
subject { entity.as_json }
it 'contains required fields' do
expect(subject).to include(:id, :name, :approval_status)
end
end
end
......@@ -116,6 +116,8 @@ describe MergeRequestWidgetEntity do
expect(subject.as_json).to include(:license_management)
expect(subject.as_json[:license_management]).to include(:head_path)
expect(subject.as_json[:license_management]).to include(:base_path)
expect(subject.as_json[:license_management]).to include(:managed_licenses_path)
expect(subject.as_json[:license_management]).to include(:can_manage_licenses)
end
# methods for old artifact are deprecated and replaced with ones for the new name (#5779)
......
# frozen_string_literal: true
require 'spec_helper'
describe SoftwareLicensePolicies::CreateService do
let(:project) { create(:project)}
let(:params) { { name: 'ExamplePL/2.1', approval_status: 'blacklisted' } }
let(:user) do
create(:user).tap do |u|
project.add_maintainer(u)
end
end
before do
stub_licensed_features(license_management: true)
end
subject { described_class.new(project, user, params).execute }
describe '#execute' do
context 'with license management unavailable' do
before do
stub_licensed_features(license_management: false)
end
it 'does not creates a software license policy' do
expect { subject }.to change { project.software_license_policies.count }.by(0)
end
end
context 'with a user who is allowed to admin' do
it 'creates one software license policy correctly' do
expect { subject }.to change { project.software_license_policies.count }.from(0).to(1)
software_license_policy = project.software_license_policies.last
expect(software_license_policy).to be_persisted
expect(software_license_policy.name).to eq('ExamplePL/2.1')
expect(software_license_policy.approval_status).to eq('blacklisted')
end
end
context 'with a user not allowed to admin' do
let(:user) { create(:user) }
it 'does not create a software license policy' do
expect { subject }.to change { project.software_license_policies.count }.by(0)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe SoftwareLicensePolicies::UpdateService do
let(:project) { create(:project)}
let(:user) do
create(:user).tap do |u|
project.add_maintainer(u)
end
end
let(:software_license_policy) do
create(
:software_license_policy,
software_license: create(:software_license, name: 'ExamplePL/2.1'),
approval_status: 'blacklisted'
)
end
before do
stub_licensed_features(license_management: true)
end
describe '#execute' do
def update_software_license_policy(opts)
described_class.new(project, user, opts).execute(software_license_policy)
end
context 'approval status update' do
let(:opts) do
{ approval_status: 'approved' }
end
context 'with license management unavailable' do
before do
stub_licensed_features(license_management: false)
end
it 'does not update the software license policy' do
update_software_license_policy(opts)
expect(software_license_policy).to be_valid
expect(software_license_policy.approval_status).not_to eq(opts[:approval_status])
end
end
context 'with a user allowed to admin' do
it 'updates the software license policy correctly' do
update_software_license_policy(opts)
expect(software_license_policy).to be_valid
expect(software_license_policy.approval_status).to eq(opts[:approval_status])
end
end
context 'with a user not allowed to admin' do
let(:user) { create(:user) }
it 'does not updates the software license policy' do
update_software_license_policy(opts)
expect(software_license_policy).to be_valid
expect(software_license_policy.approval_status).not_to eq(opts[:approval_status])
end
end
end
context 'name update' do
let(:opts) do
{ name: 'MyPL' }
end
it 'does not updates the software license policy' do
update_software_license_policy(opts)
expect(software_license_policy).to be_valid
expect(software_license_policy.name).not_to eq(opts[:name])
end
end
end
end
......@@ -116,6 +116,7 @@ module API
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
mount ::API::ManagedLicenses
mount ::API::Markdown
mount ::API::Members
mount ::API::MergeRequestApprovals
......
......@@ -1422,6 +1422,10 @@ module API
expose :reset_approvals_on_push
expose :disable_overriding_approvers_per_merge_request
end
class ManagedLicense < Grape::Entity
expose :id, :name, :approval_status
end
end
end
......
......@@ -334,6 +334,7 @@ project:
- import_export_upload
- vulnerability_feedback
- prometheus_alerts
- software_license_policies
award_emoji:
- awardable
- user
......
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