Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
heptapod
heptapod
Commits
8eb43f5ba92c
Commit
e9eae3eb
authored
Sep 28, 2017
by
Markus Koller
Committed by
Rémy Coutable
Sep 28, 2017
Browse files
Support custom attributes on users
parent
96c1b7354720
Changes
23
Hide whitespace changes
Inline
Side-by-side
app/finders/concerns/custom_attributes_filter.rb
0 → 100644
View file @
8eb43f5b
module
CustomAttributesFilter
def
by_custom_attributes
(
items
)
return
items
unless
params
[
:custom_attributes
].
is_a?
(
Hash
)
return
items
unless
Ability
.
allowed?
(
current_user
,
:read_custom_attribute
)
association
=
items
.
reflect_on_association
(
:custom_attributes
)
attributes_table
=
association
.
klass
.
arel_table
attributable_table
=
items
.
model
.
arel_table
custom_attributes
=
association
.
klass
.
select
(
'true'
).
where
(
attributes_table
[
association
.
foreign_key
]
.
eq
(
attributable_table
[
association
.
association_primary_key
])
)
# perform a subquery for each attribute to be filtered
params
[
:custom_attributes
].
inject
(
items
)
do
|
scope
,
(
key
,
value
)
|
scope
.
where
(
'EXISTS (?)'
,
custom_attributes
.
where
(
key:
key
,
value:
value
))
end
end
end
app/finders/users_finder.rb
View file @
8eb43f5b
...
...
@@ -15,6 +15,7 @@
#
class
UsersFinder
include
CreatedAtFilter
include
CustomAttributesFilter
attr_accessor
:current_user
,
:params
...
...
@@ -32,6 +33,7 @@ def execute
users
=
by_external_identity
(
users
)
users
=
by_external
(
users
)
users
=
by_created_at
(
users
)
users
=
by_custom_attributes
(
users
)
users
end
...
...
app/models/user.rb
View file @
8eb43f5b
...
...
@@ -130,6 +130,8 @@ def update_tracked_fields!(request)
has_many
:assigned_issues
,
class_name:
"Issue"
,
through: :issue_assignees
,
source: :issue
has_many
:assigned_merge_requests
,
dependent: :nullify
,
foreign_key: :assignee_id
,
class_name:
"MergeRequest"
# rubocop:disable Cop/ActiveRecordDependent
has_many
:custom_attributes
,
class_name:
'UserCustomAttribute'
#
# Validations
#
...
...
app/models/user_custom_attribute.rb
0 → 100644
View file @
8eb43f5b
class
UserCustomAttribute
<
ActiveRecord
::
Base
belongs_to
:user
validates
:user_id
,
:key
,
:value
,
presence:
true
validates
:key
,
uniqueness:
{
scope:
[
:user_id
]
}
end
app/policies/global_policy.rb
View file @
8eb43f5b
...
...
@@ -47,4 +47,9 @@ class GlobalPolicy < BasePolicy
rule
{
~
(
anonymous
&
restricted_public_level
)
}.
policy
do
enable
:read_users_list
end
rule
{
admin
}.
policy
do
enable
:read_custom_attribute
enable
:update_custom_attribute
end
end
changelogs/unreleased/feature-custom-attributes.yml
0 → 100644
View file @
8eb43f5b
---
title
:
Support custom attributes on users
merge_request
:
13038
author
:
Markus Koller
db/migrate/20170720122741_create_user_custom_attributes.rb
0 → 100644
View file @
8eb43f5b
class
CreateUserCustomAttributes
<
ActiveRecord
::
Migration
include
Gitlab
::
Database
::
MigrationHelpers
DOWNTIME
=
false
def
change
create_table
:user_custom_attributes
do
|
t
|
t
.
timestamps_with_timezone
null:
false
t
.
references
:user
,
null:
false
,
foreign_key:
{
on_delete: :cascade
}
t
.
string
:key
,
null:
false
t
.
string
:value
,
null:
false
t
.
index
[
:user_id
,
:key
],
unique:
true
t
.
index
[
:key
,
:value
]
end
end
end
db/schema.rb
View file @
8eb43f5b
...
...
@@ -1534,6 +1534,17 @@
add_index
"user_agent_details"
,
[
"subject_id"
,
"subject_type"
],
name:
"index_user_agent_details_on_subject_id_and_subject_type"
,
using: :btree
create_table
"user_custom_attributes"
,
force: :cascade
do
|
t
|
t
.
datetime
"created_at"
,
null:
false
t
.
datetime
"updated_at"
,
null:
false
t
.
integer
"user_id"
,
null:
false
t
.
string
"key"
,
null:
false
t
.
string
"value"
,
null:
false
end
add_index
"user_custom_attributes"
,
[
"key"
,
"value"
],
name:
"index_user_custom_attributes_on_key_and_value"
,
using: :btree
add_index
"user_custom_attributes"
,
[
"user_id"
,
"key"
],
name:
"index_user_custom_attributes_on_user_id_and_key"
,
unique:
true
,
using: :btree
create_table
"user_synced_attributes_metadata"
,
force: :cascade
do
|
t
|
t
.
boolean
"name_synced"
,
default:
false
t
.
boolean
"email_synced"
,
default:
false
...
...
@@ -1760,6 +1771,7 @@
add_foreign_key
"todos"
,
"projects"
,
name:
"fk_45054f9c45"
,
on_delete: :cascade
add_foreign_key
"trending_projects"
,
"projects"
,
on_delete: :cascade
add_foreign_key
"u2f_registrations"
,
"users"
add_foreign_key
"user_custom_attributes"
,
"users"
,
on_delete: :cascade
add_foreign_key
"user_synced_attributes_metadata"
,
"users"
,
on_delete: :cascade
add_foreign_key
"users_star_projects"
,
"projects"
,
name:
"fk_22cd27ddfc"
,
on_delete: :cascade
add_foreign_key
"web_hook_logs"
,
"web_hooks"
,
on_delete: :cascade
...
...
doc/api/README.md
View file @
8eb43f5b
...
...
@@ -14,6 +14,7 @@ following locations:
-
[
Project-level Variables
](
project_level_variables.md
)
-
[
Group-level Variables
](
group_level_variables.md
)
-
[
Commits
](
commits.md
)
-
[
Custom Attributes
](
custom_attributes.md
)
-
[
Deployments
](
deployments.md
)
-
[
Deploy Keys
](
deploy_keys.md
)
-
[
Environments
](
environments.md
)
...
...
doc/api/custom_attributes.md
0 → 100644
View file @
8eb43f5b
# Custom Attributes API
Every API call to custom attributes must be authenticated as administrator.
## List custom attributes
Get all custom attributes on a user.
```
GET /users/:id/custom_attributes
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a user |
```
bash
curl
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v4/users/42/custom_attributes
```
Example response:
```
json
[
{
"key"
:
"location"
,
"value"
:
"Antarctica"
},
{
"key"
:
"role"
,
"value"
:
"Developer"
}
]
```
## Single custom attribute
Get a single custom attribute on a user.
```
GET /users/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a user |
|
`key`
| string | yes | The key of the custom attribute |
```
bash
curl
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v4/users/42/custom_attributes/location
```
Example response:
```
json
{
"key"
:
"location"
,
"value"
:
"Antarctica"
}
```
## Set custom attribute
Set a custom attribute on a user. The attribute will be updated if it already exists,
or newly created otherwise.
```
PUT /users/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a user |
|
`key`
| string | yes | The key of the custom attribute |
|
`value`
| string | yes | The value of the custom attribute |
```
bash
curl
--request
PUT
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
--data
"value=Greenland"
https://gitlab.example.com/api/v4/users/42/custom_attributes/location
```
Example response:
```
json
{
"key"
:
"location"
,
"value"
:
"Greenland"
}
```
## Delete custom attribute
Delete a custom attribute on a user.
```
DELETE /users/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a user |
|
`key`
| string | yes | The key of the custom attribute |
```
bash
curl
--request
DELETE
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v4/users/42/custom_attributes/location
```
doc/api/users.md
View file @
8eb43f5b
...
...
@@ -154,6 +154,12 @@ You can search users by creation date time range with:
GET /users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060
```
You can filter by
[
custom attributes
](
custom_attributes.md
)
with:
```
GET /users?custom_attributes[key]=value&custom_attributes[other_key]=other_value
```
## Single user
Get a single user.
...
...
lib/api/custom_attributes_endpoints.rb
0 → 100644
View file @
8eb43f5b
module
API
module
CustomAttributesEndpoints
extend
ActiveSupport
::
Concern
included
do
attributable_class
=
name
.
demodulize
.
singularize
attributable_key
=
attributable_class
.
underscore
attributable_name
=
attributable_class
.
humanize
(
capitalize:
false
)
attributable_finder
=
"find_
#{
attributable_key
}
"
helpers
do
params
:custom_attributes_key
do
requires
:key
,
type:
String
,
desc:
'The key of the custom attribute'
end
end
desc
"Get all custom attributes on a
#{
attributable_name
}
"
do
success
Entities
::
CustomAttribute
end
get
':id/custom_attributes'
do
resource
=
public_send
(
attributable_finder
,
params
[
:id
])
# rubocop:disable GitlabSecurity/PublicSend
authorize!
:read_custom_attribute
present
resource
.
custom_attributes
,
with:
Entities
::
CustomAttribute
end
desc
"Get a custom attribute on a
#{
attributable_name
}
"
do
success
Entities
::
CustomAttribute
end
params
do
use
:custom_attributes_key
end
get
':id/custom_attributes/:key'
do
resource
=
public_send
(
attributable_finder
,
params
[
:id
])
# rubocop:disable GitlabSecurity/PublicSend
authorize!
:read_custom_attribute
custom_attribute
=
resource
.
custom_attributes
.
find_by!
(
key:
params
[
:key
])
present
custom_attribute
,
with:
Entities
::
CustomAttribute
end
desc
"Set a custom attribute on a
#{
attributable_name
}
"
params
do
use
:custom_attributes_key
requires
:value
,
type:
String
,
desc:
'The value of the custom attribute'
end
put
':id/custom_attributes/:key'
do
resource
=
public_send
(
attributable_finder
,
params
[
:id
])
# rubocop:disable GitlabSecurity/PublicSend
authorize!
:update_custom_attribute
custom_attribute
=
resource
.
custom_attributes
.
find_or_initialize_by
(
key:
params
[
:key
])
custom_attribute
.
update
(
value:
params
[
:value
])
if
custom_attribute
.
valid?
present
custom_attribute
,
with:
Entities
::
CustomAttribute
else
render_validation_error!
(
custom_attribute
)
end
end
desc
"Delete a custom attribute on a
#{
attributable_name
}
"
params
do
use
:custom_attributes_key
end
delete
':id/custom_attributes/:key'
do
resource
=
public_send
(
attributable_finder
,
params
[
:id
])
# rubocop:disable GitlabSecurity/PublicSend
authorize!
:update_custom_attribute
resource
.
custom_attributes
.
find_by!
(
key:
params
[
:key
]).
destroy
status
204
end
end
end
end
lib/api/entities.rb
View file @
8eb43f5b
...
...
@@ -1036,5 +1036,10 @@ class RepositoryStorageHealth < Grape::Entity
expose
:failing_on_hosts
expose
:total_failures
end
class
CustomAttribute
<
Grape
::
Entity
expose
:key
expose
:value
end
end
end
lib/api/users.rb
View file @
8eb43f5b
...
...
@@ -6,6 +6,8 @@ class Users < Grape::API
allow_access_with_scope
:read_user
,
if:
->
(
request
)
{
request
.
get?
}
resource
:users
,
requirements:
{
uid:
/[0-9]*/
,
id:
/[0-9]*/
}
do
include
CustomAttributesEndpoints
before
do
authenticate_non_get!
end
...
...
spec/factories/user_custom_attributes.rb
0 → 100644
View file @
8eb43f5b
FactoryGirl
.
define
do
factory
:user_custom_attribute
do
user
sequence
(
:key
)
{
|
n
|
"key
#{
n
}
"
}
sequence
(
:value
)
{
|
n
|
"value
#{
n
}
"
}
end
end
spec/finders/users_finder_spec.rb
View file @
8eb43f5b
...
...
@@ -56,6 +56,15 @@
expect
(
users
.
map
(
&
:username
)).
not_to
include
([
filtered_user_before
.
username
,
filtered_user_after
.
username
])
end
it
'does not filter by custom attributes'
do
users
=
described_class
.
new
(
user
,
custom_attributes:
{
foo:
'bar'
}
).
execute
expect
(
users
).
to
contain_exactly
(
user
,
user1
,
user2
,
omniauth_user
)
end
end
context
'with an admin user'
do
...
...
@@ -72,6 +81,19 @@
expect
(
users
).
to
contain_exactly
(
admin
,
user1
,
user2
,
external_user
,
omniauth_user
)
end
it
'filters by custom attributes'
do
create
:user_custom_attribute
,
user:
user1
,
key:
'foo'
,
value:
'foo'
create
:user_custom_attribute
,
user:
user1
,
key:
'bar'
,
value:
'bar'
create
:user_custom_attribute
,
user:
user2
,
key:
'foo'
,
value:
'foo'
users
=
described_class
.
new
(
admin
,
custom_attributes:
{
foo:
'foo'
,
bar:
'bar'
}
).
execute
expect
(
users
).
to
contain_exactly
(
user1
)
end
end
end
end
spec/models/ci/pipeline_variable_spec.rb
View file @
8eb43f5b
require
'spec_helper'
describe
Ci
::
PipelineVariable
,
models:
true
do
describe
Ci
::
PipelineVariable
do
subject
{
build
(
:ci_pipeline_variable
)
}
it
{
is_expected
.
to
include_module
(
HasVariable
)
}
...
...
spec/models/repository_spec.rb
View file @
8eb43f5b
require
'spec_helper'
describe
Repository
,
models:
true
do
describe
Repository
do
include
RepoHelpers
TestBlob
=
Struct
.
new
(
:path
)
...
...
spec/models/user_custom_attribute_spec.rb
0 → 100644
View file @
8eb43f5b
require
'spec_helper'
describe
UserCustomAttribute
do
describe
'assocations'
do
it
{
is_expected
.
to
belong_to
(
:user
)
}
end
describe
'validations'
do
subject
{
build
:user_custom_attribute
}
it
{
is_expected
.
to
validate_presence_of
(
:user_id
)
}
it
{
is_expected
.
to
validate_presence_of
(
:key
)
}
it
{
is_expected
.
to
validate_presence_of
(
:value
)
}
it
{
is_expected
.
to
validate_uniqueness_of
(
:key
).
scoped_to
(
:user_id
)
}
end
end
spec/models/user_spec.rb
View file @
8eb43f5b
...
...
@@ -39,6 +39,7 @@
it
{
is_expected
.
to
have_many
(
:chat_names
).
dependent
(
:destroy
)
}
it
{
is_expected
.
to
have_many
(
:uploads
).
dependent
(
:destroy
)
}
it
{
is_expected
.
to
have_many
(
:reported_abuse_reports
).
dependent
(
:destroy
).
class_name
(
'AbuseReport'
)
}
it
{
is_expected
.
to
have_many
(
:custom_attributes
).
class_name
(
'UserCustomAttribute'
)
}
describe
"#abuse_report"
do
let
(
:current_user
)
{
create
(
:user
)
}
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment