Commit 21315910 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'ff-lists-api' into 'master'

Add Public API for Feature Flag User Lists

See merge request gitlab-org/gitlab!29415
parents b2501d91 02d54ff3
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module InternalIdEnums module InternalIdEnums
def self.usage_resources def self.usage_resources
# when adding new resource, make sure it doesn't conflict with EE usage_resources # when adding new resource, make sure it doesn't conflict with EE usage_resources
{ issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6 } { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6, operations_user_lists: 7 }
end end
end end
......
---
title: Add public API for feature flag user lists
merge_request: 29415
author:
type: added
...@@ -31,6 +31,8 @@ The following API resources are available in the project context: ...@@ -31,6 +31,8 @@ The following API resources are available in the project context:
| [Environments](environments.md) | `/projects/:id/environments` | | [Environments](environments.md) | `/projects/:id/environments` |
| [Error Tracking](error_tracking.md) | `/projects/:id/error_tracking/settings` | | [Error Tracking](error_tracking.md) | `/projects/:id/error_tracking/settings` |
| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) | | [Events](events.md) | `/projects/:id/events` (also available for users and standalone) |
| [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` |
| [Feature Flag User Lists](feature_flag_user_lists.md) | `/projects/:id/feature_flags_user_lists` |
| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) | | [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) |
| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) | | [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) |
| [Issue boards](boards.md) | `/projects/:id/boards` | | [Issue boards](boards.md) | `/projects/:id/boards` |
......
# Feature Flag User Lists API **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205409) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.10.
API for accessing GitLab Feature Flag User Lists.
Users with Developer or higher [permissions](../user/permissions.md) can access the Feature Flag User Lists API.
NOTE: **Note:**
`GET` requests return twenty results at a time because the API results
are [paginated](README.md#pagination). You can change this value.
## List all feature flag user lists for a project
Gets all feature flag user lists for the requested project.
```plaintext
GET /projects/:id/feature_flags_user_lists
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists
```
Example response:
```json
[
{
"name": "user_list",
"user_xids": "user1,user2",
"id": 1,
"iid": 1,
"project_id": 1,
"created_at": "2020-02-04T08:13:51.423Z",
"updated_at": "2020-02-04T08:13:51.423Z"
},
{
"name": "test_users",
"user_xids": "user3,user4,user5",
"id": 2,
"iid": 2,
"project_id": 1,
"created_at": "2020-02-04T08:13:10.507Z",
"updated_at": "2020-02-04T08:13:10.507Z"
}
]
```
## Create a feature flag user list
Creates a feature flag user list.
```plaintext
POST /projects/:id/feature_flags_user_lists
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `name` | string | yes | The name of the feature flag. |
| `user_xids` | string | yes | A comma separated list of user ids. |
```shell
curl https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-type: application/json" \
--data @- << EOF
{
"name": "my_user_list",
"user_xids": "user1,user2,user3"
}
EOF
```
Example response:
```json
{
"name": "my_user_list",
"user_xids": "user1,user2,user3",
"id": 1,
"iid": 1,
"project_id": 1,
"created_at": "2020-02-04T08:32:27.288Z",
"updated_at": "2020-02-04T08:32:27.288Z"
}
```
## Get a feature flag user list
Gets a feature flag user list.
```plaintext
GET /projects/:id/feature_flags_user_lists/:iid
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `iid` | integer/string | yes | The internal ID of the project's feature flag user list. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1
```
Example response:
```json
{
"name": "my_user_list",
"user_xids": "123,456",
"id": 1,
"iid": 1,
"project_id": 1,
"created_at": "2020-02-04T08:13:10.507Z",
"updated_at": "2020-02-04T08:13:10.507Z",
}
```
## Delete feature flag user list
Deletes a feature flag user list.
```plaintext
DELETE /projects/:id/feature_flags_user_lists/:iid
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `iid` | integer/string | yes | The internal ID of the project's feature flag user list |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1
```
...@@ -265,3 +265,4 @@ to control them in an automated flow: ...@@ -265,3 +265,4 @@ to control them in an automated flow:
- [Feature Flags API](../../../api/feature_flags.md) - [Feature Flags API](../../../api/feature_flags.md)
- [Feature Flag Specs API](../../../api/feature_flag_specs.md) - [Feature Flag Specs API](../../../api/feature_flag_specs.md)
- [Feature Flag User Lists API](../../../api/feature_flag_user_lists.md)
...@@ -85,6 +85,7 @@ module EE ...@@ -85,6 +85,7 @@ module EE
has_many :operations_feature_flags, class_name: 'Operations::FeatureFlag' has_many :operations_feature_flags, class_name: 'Operations::FeatureFlag'
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient' has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
has_many :project_aliases has_many :project_aliases
......
# frozen_string_literal: true
module Operations
module FeatureFlags
class UserList < ApplicationRecord
include AtomicInternalId
USERXID_MAX_LENGTH = 256
self.table_name = 'operations_user_lists'
belongs_to :project
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.operations_feature_flags_user_lists&.maximum(:iid) }, presence: true
validates :project, presence: true
validates :name,
presence: true,
uniqueness: { scope: :project_id },
length: 1..255
validate :user_xids_validation
private
def user_xids_validation
unless user_xids.is_a?(String) && !user_xids.match(/[\n\r\t]|,,/) && valid_xids?(user_xids.split(","))
errors.add(:user_xids,
"user_xids must be a string of unique comma separated values each #{USERXID_MAX_LENGTH} characters or less")
end
end
def valid_xids?(user_xids)
user_xids.uniq.length == user_xids.length &&
user_xids.all? { |xid| valid_xid?(xid) }
end
def valid_xid?(user_xid)
user_xid.present? &&
user_xid.strip == user_xid &&
user_xid.length <= USERXID_MAX_LENGTH
end
end
end
end
...@@ -181,6 +181,7 @@ module EE ...@@ -181,6 +181,7 @@ module EE
enable :update_feature_flag enable :update_feature_flag
enable :destroy_feature_flag enable :destroy_feature_flag
enable :admin_feature_flag enable :admin_feature_flag
enable :admin_feature_flags_user_lists
enable :create_design enable :create_design
enable :destroy_design enable :destroy_design
end end
...@@ -217,6 +218,7 @@ module EE ...@@ -217,6 +218,7 @@ module EE
rule { feature_flags_disabled | repository_disabled }.policy do rule { feature_flags_disabled | repository_disabled }.policy do
prevent(*create_read_update_admin_destroy(:feature_flag)) prevent(*create_read_update_admin_destroy(:feature_flag))
prevent(:admin_feature_flags_user_lists)
end end
rule { can?(:maintainer_access) }.policy do rule { can?(:maintainer_access) }.policy do
......
# frozen_string_literal: true
module API
class FeatureFlagsUserLists < Grape::API
include PaginationParams
error_formatter :json, -> (message, _backtrace, _options, _env, _original_exception) {
message.is_a?(String) ? { message: message }.to_json : message.to_json
}
before do
authorize_admin_feature_flags_user_lists!
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
resource :feature_flags_user_lists do
desc 'Get all feature flags user lists of a project' do
detail 'This feature was introduced in GitLab 12.10'
success EE::API::Entities::FeatureFlag::UserList
end
params do
use :pagination
end
get do
present paginate(user_project.operations_feature_flags_user_lists),
with: EE::API::Entities::FeatureFlag::UserList
end
desc 'Create a feature flags user list for a project' do
detail 'This feature was introduced in GitLab 12.10'
success EE::API::Entities::FeatureFlag::UserList
end
params do
requires :name, type: String, desc: 'The name of the list'
requires :user_xids, type: String, desc: 'A comma separated list of external user ids'
end
post do
list = user_project.operations_feature_flags_user_lists.create(declared_params)
if list.save
present list, with: EE::API::Entities::FeatureFlag::UserList
else
render_api_error!(list.errors.full_messages, :bad_request)
end
end
end
params do
requires :iid, type: String, desc: 'The internal id of the user list'
end
resource 'feature_flags_user_lists/:iid' do
desc 'Get a single feature flag user list belonging to a project' do
detail 'This feature was introduced in GitLab 12.10'
success EE::API::Entities::FeatureFlag::UserList
end
get do
present user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid]),
with: EE::API::Entities::FeatureFlag::UserList
end
desc 'Update a feature flag user list' do
detail 'This feature was introduced in GitLab 12.10'
success EE::API::Entities::FeatureFlag::UserList
end
params do
optional :name, type: String, desc: 'The name of the list'
optional :user_xids, type: String, desc: 'A comma separated list of external user ids'
end
put do
list = user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid])
if list.update(declared_params(include_missing: false))
present list, with: EE::API::Entities::FeatureFlag::UserList
else
render_api_error!(list.errors.full_messages, :bad_request)
end
end
desc 'Delete a feature flag user list' do
detail 'This feature was introduced in GitLab 12.10'
end
delete do
list = user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid])
list.destroy
end
end
end
helpers do
def authorize_admin_feature_flags_user_lists!
authorize! :admin_feature_flags_user_lists, user_project
end
end
end
end
...@@ -21,6 +21,7 @@ module EE ...@@ -21,6 +21,7 @@ module EE
mount ::API::Epics mount ::API::Epics
mount ::API::ElasticsearchIndexedNamespaces mount ::API::ElasticsearchIndexedNamespaces
mount ::API::FeatureFlags mount ::API::FeatureFlags
mount ::API::FeatureFlagsUserLists
mount ::API::FeatureFlagScopes mount ::API::FeatureFlagScopes
mount ::API::Geo mount ::API::Geo
mount ::API::GeoReplication mount ::API::GeoReplication
......
# frozen_string_literal: true
module EE
module API
module Entities
class FeatureFlag < Grape::Entity
class UserList < Grape::Entity
expose :id
expose :iid
expose :project_id
expose :created_at
expose :updated_at
expose :name
expose :user_xids
end
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :operations_feature_flag_user_list, class: 'Operations::FeatureFlags::UserList' do
association :project, factory: :project
name { 'My User List' }
user_xids { 'user1,user2,user3' }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Operations::FeatureFlags::UserList do
subject { create(:operations_feature_flag_user_list) }
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:name).is_at_least(1).is_at_most(255) }
describe 'user_xids' do
where(:valid_value) do
["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike",
"gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0",
"$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed",
"a" * 256, "a,#{'b' * 256},ccc", "many spaces"]
end
with_them do
it 'is valid with a string of comma separated values' do
user_list = described_class.create(user_xids: valid_value)
expect(user_list.errors[:user_xids]).to be_empty
end
end
where(:typecast_value) do
[1, 2.5, {}, []]
end
with_them do
it 'automatically casts values of other types' do
user_list = described_class.create(user_xids: typecast_value)
expect(user_list.errors[:user_xids]).to be_empty
expect(user_list.user_xids).to eq(typecast_value.to_s)
end
end
where(:invalid_value) do
[nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r",
"joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ",
" ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"]
end
with_them do
it 'is invalid' do
user_list = described_class.create(user_xids: invalid_value)
expect(user_list.errors[:user_xids]).to include(
'user_xids must be a string of unique comma separated values each 256 characters or less'
)
end
end
end
end
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:operations_feature_flag_user_list) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :operations_user_lists }
end
end
This diff is collapsed.
...@@ -443,6 +443,7 @@ project: ...@@ -443,6 +443,7 @@ project:
- vulnerability_scanners - vulnerability_scanners
- operations_feature_flags - operations_feature_flags
- operations_feature_flags_client - operations_feature_flags_client
- operations_feature_flags_user_lists
- prometheus_alerts - prometheus_alerts
- prometheus_alert_events - prometheus_alert_events
- self_managed_prometheus_alert_events - self_managed_prometheus_alert_events
......
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