mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-25 19:56:09 +01:00
Merge pull request #14604 from reasonerjt/users-api-refact-2
API for users to new model
This commit is contained in:
commit
1d01db3d3c
@ -356,366 +356,6 @@ paths:
|
||||
description: User need to log in first.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
/users:
|
||||
get:
|
||||
summary: Get registered users of Harbor.
|
||||
description: |
|
||||
This endpoint is for user to search registered users, support for filtering results with username.Notice, by now this operation is only for administrator.
|
||||
parameters:
|
||||
- name: username
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: Username for filtering results.
|
||||
- name: email
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: Email for filtering results.
|
||||
- name: page
|
||||
in: query
|
||||
type: integer
|
||||
format: int32
|
||||
required: false
|
||||
description: 'The page number, default is 1.'
|
||||
- name: page_size
|
||||
in: query
|
||||
type: integer
|
||||
format: int32
|
||||
required: false
|
||||
description: The size of per page.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Searched for users of Harbor successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/User'
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of available items
|
||||
type: integer
|
||||
Link:
|
||||
description: Link to previous page and next page
|
||||
type: string
|
||||
'400':
|
||||
description: Invalid user ID.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission of admin role.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
post:
|
||||
summary: Creates a new user account.
|
||||
description: |
|
||||
This endpoint is to create a user if the user does not already exist.
|
||||
parameters:
|
||||
- name: user
|
||||
in: body
|
||||
description: New created user.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'201':
|
||||
description: User created successfully.
|
||||
headers:
|
||||
Location:
|
||||
type: string
|
||||
description: The URL of the created resource
|
||||
'400':
|
||||
description: Unsatisfied with constraints of the user creation.
|
||||
'403':
|
||||
description: User registration can only be used by admin role user when self-registration is off.
|
||||
'415':
|
||||
$ref: '#/responses/UnsupportedMediaType'
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
/users/current:
|
||||
get:
|
||||
summary: Get current user info.
|
||||
description: |
|
||||
This endpoint is to get the current user information.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get current user information successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
/users/current/permissions:
|
||||
get:
|
||||
summary: Get current user permissions.
|
||||
description: |
|
||||
This endpoint is to get the current user permissions.
|
||||
parameters:
|
||||
- name: scope
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: Get permissions of the scope
|
||||
- name: relative
|
||||
in: query
|
||||
type: boolean
|
||||
required: false
|
||||
description: |
|
||||
If true, the resources in the response are relative to the scope,
|
||||
eg for resource '/project/1/repository' if relative is 'true' then the resource in response will be 'repository'.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get current user permission successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Permission'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'500':
|
||||
description: Internal errors.
|
||||
/users/search:
|
||||
get:
|
||||
summary: Search users by username
|
||||
description: |
|
||||
This endpoint is to search the users by username.
|
||||
parameters:
|
||||
- name: username
|
||||
in: query
|
||||
type: string
|
||||
required: true
|
||||
description: Username for filtering results.
|
||||
- name: page
|
||||
in: query
|
||||
type: integer
|
||||
format: int32
|
||||
required: false
|
||||
description: 'The page number, default is 1.'
|
||||
- name: page_size
|
||||
in: query
|
||||
type: integer
|
||||
format: int32
|
||||
required: false
|
||||
description: The size of per page.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Search users by username, email successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/UserSearch'
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of available items
|
||||
type: integer
|
||||
Link:
|
||||
description: Link to previous page and next page
|
||||
type: string
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/users/{user_id}':
|
||||
get:
|
||||
summary: Get a user's profile.
|
||||
description: |
|
||||
Get user's profile with user id.
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: Registered user ID
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get user's profile successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
||||
'400':
|
||||
description: Invalid user ID.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission of admin role.
|
||||
'404':
|
||||
description: User ID does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
put:
|
||||
summary: Update a registered user to change his profile.
|
||||
description: |
|
||||
This endpoint let a registered user change his profile.
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: Registered user ID
|
||||
- name: profile
|
||||
in: body
|
||||
description: 'Only email, realname and comment can be modified.'
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/UserProfile'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Updated user's profile successfully.
|
||||
'400':
|
||||
description: Invalid user ID.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission of admin role.
|
||||
'404':
|
||||
description: User ID does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
delete:
|
||||
summary: Mark a registered user as be removed.
|
||||
description: |
|
||||
This endpoint let administrator of Harbor mark a registered user as
|
||||
be removed.It actually won't be deleted from DB.
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: User ID for marking as to be removed.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Marked user as be removed successfully.
|
||||
'400':
|
||||
description: Invalid user ID.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission of admin role.
|
||||
'404':
|
||||
description: User ID does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/users/{user_id}/password':
|
||||
put:
|
||||
summary: Change the password on a user that already exists.
|
||||
description: |
|
||||
This endpoint is for user to update password. Users with the admin role can change any user's password. Guest users can change only their own password.
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: Registered user ID.
|
||||
- name: password
|
||||
in: body
|
||||
description: Password to be updated, the attribute 'old_password' is optional when the API is called by the system administrator.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Password'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Updated password successfully.
|
||||
'400':
|
||||
description: Invalid user ID; Old password is blank; New password is blank.
|
||||
'401':
|
||||
description: Don't have authority to change password. Please check login status.
|
||||
'403':
|
||||
description: The caller does not have permission to update the password of the user with given ID, or the old password in request body is not correct.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/users/{user_id}/sysadmin':
|
||||
put:
|
||||
summary: Update a registered user to change to be an administrator of Harbor.
|
||||
description: |
|
||||
This endpoint let a registered user change to be an administrator
|
||||
of Harbor.
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: Registered user ID
|
||||
- name: sysadmin_flag
|
||||
in: body
|
||||
description: Toggle a user to admin or not.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/SysAdminFlag'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Updated user's admin role successfully.
|
||||
'400':
|
||||
description: Invalid user ID.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission of admin role.
|
||||
'404':
|
||||
description: User ID does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/users/{user_id}/cli_secret':
|
||||
put:
|
||||
summary: Set CLI secret for a user.
|
||||
description: |
|
||||
This endpoint let user generate a new CLI secret for himself. This API only works when auth mode is set to 'OIDC'.
|
||||
Once this API returns with successful status, the old secret will be invalid, as there will be only one CLI secret
|
||||
for a user.
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: User ID
|
||||
- name: input_secret
|
||||
in: body
|
||||
description: JSON object that includes the new secret
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
secret:
|
||||
type: string
|
||||
description: The new secret
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: The secret is successfully updated
|
||||
'400':
|
||||
description: Invalid user ID. Or user is not onboarded via OIDC authentication. Or the secret does not meet the standard.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: Non-admin user can only generate the cli secret of himself.
|
||||
'404':
|
||||
description: User ID does not exist.
|
||||
'412':
|
||||
description: The auth mode of the system is not "oidc_auth", or the user is not onboarded via OIDC AuthN.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
/labels:
|
||||
get:
|
||||
summary: List labels according to the query strings.
|
||||
@ -1117,61 +757,6 @@ definitions:
|
||||
type: string
|
||||
description: 'Whether this project reuse the system level CVE allowlist as the allowlist of its own. The valid values are "true", "false".
|
||||
If it is set to "true" the actual allowlist associate with this project, if any, will be ignored.'
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: integer
|
||||
format: int
|
||||
description: The ID of the user.
|
||||
username:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
realname:
|
||||
type: string
|
||||
comment:
|
||||
type: string
|
||||
deleted:
|
||||
type: boolean
|
||||
role_name:
|
||||
type: string
|
||||
role_id:
|
||||
type: integer
|
||||
format: int
|
||||
sysadmin_flag:
|
||||
type: boolean
|
||||
admin_role_in_auth:
|
||||
type: boolean
|
||||
description: indicate the admin privilege is grant by authenticator (LDAP), is always false unless it is the current login user
|
||||
reset_uuid:
|
||||
type: string
|
||||
Salt:
|
||||
type: string
|
||||
creation_time:
|
||||
type: string
|
||||
update_time:
|
||||
type: string
|
||||
UserSearch:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: integer
|
||||
format: int
|
||||
description: The ID of the user.
|
||||
username:
|
||||
type: string
|
||||
Password:
|
||||
type: object
|
||||
properties:
|
||||
old_password:
|
||||
type: string
|
||||
description: The user's existing password.
|
||||
new_password:
|
||||
type: string
|
||||
description: New password for marking as to be updated.
|
||||
Role:
|
||||
type: object
|
||||
properties:
|
||||
@ -1226,24 +811,6 @@ definitions:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 'The count of the total repositories, only be seen when the user is admin.'
|
||||
SysAdminFlag:
|
||||
type: object
|
||||
properties:
|
||||
sysadmin_flag:
|
||||
type: boolean
|
||||
description: 'true-admin, false-not admin.'
|
||||
UserProfile:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
description: The new email.
|
||||
realname:
|
||||
type: string
|
||||
description: The new realname.
|
||||
comment:
|
||||
type: string
|
||||
description: The new comment.
|
||||
LdapConf:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -4317,7 +4317,326 @@ paths:
|
||||
$ref: '#/responses/403'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
|
||||
/users:
|
||||
get:
|
||||
summary: List users
|
||||
tags:
|
||||
- users
|
||||
operationId: listUsers
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/query'
|
||||
- $ref: '#/parameters/sort'
|
||||
- $ref: '#/parameters/page'
|
||||
- $ref: '#/parameters/pageSize'
|
||||
responses:
|
||||
'200':
|
||||
description: return the list of users.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/UserResp'
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of users
|
||||
type: integer
|
||||
Link:
|
||||
description: Link to previous page and next page
|
||||
type: string
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
post:
|
||||
summary: Create a local user.
|
||||
description: This API can be used only when the authentication mode is for local DB. When self registration is disabled.
|
||||
tags:
|
||||
- users
|
||||
operationId: createUser
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- name: userReq
|
||||
in: body
|
||||
description: The new user
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/UserCreationReq'
|
||||
responses:
|
||||
'201':
|
||||
$ref: '#/responses/201'
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
description: When the self registration is disabled, non-admin does not have permission to create user. When self registration is enabled, this API can only be called from UI portal, calling it via script will get a 403 error.
|
||||
'409':
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/users/current:
|
||||
get:
|
||||
summary: Get current user info.
|
||||
tags:
|
||||
- users
|
||||
operationId: getCurrentUserInfo
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
responses:
|
||||
'200':
|
||||
description: Get current user information successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/UserResp'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/users/search:
|
||||
get:
|
||||
summary: Search users by username
|
||||
description: |
|
||||
This endpoint is to search the users by username. It's open for all authenticated requests.
|
||||
tags:
|
||||
- users
|
||||
operationId: searchUsers
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/page'
|
||||
- $ref: '#/parameters/pageSize'
|
||||
- name: username
|
||||
in: query
|
||||
type: string
|
||||
required: true
|
||||
description: Username for filtering results.
|
||||
responses:
|
||||
'200':
|
||||
description: Search users by username successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/UserSearchRespItem'
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of available items
|
||||
type: integer
|
||||
Link:
|
||||
description: Link to previous page and next page
|
||||
type: string
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
'/users/{user_id}':
|
||||
get:
|
||||
summary: Get a user's profile.
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
tags:
|
||||
- users
|
||||
operationId: getUser
|
||||
responses:
|
||||
'200':
|
||||
description: Get user's info successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/UserResp'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
put:
|
||||
summary: Update user's profile.
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: Registered user ID
|
||||
- name: profile
|
||||
in: body
|
||||
description: 'Only email, realname and comment can be modified.'
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/UserProfile'
|
||||
tags:
|
||||
- users
|
||||
operationId: updateUserProfile
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/200'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
delete:
|
||||
summary: Mark a registered user as be removed.
|
||||
description: |
|
||||
This endpoint let administrator of Harbor mark a registered user as removed.It actually won't be deleted from DB.
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: User ID for marking as to be removed.
|
||||
tags:
|
||||
- users
|
||||
operationId: deleteUser
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/200'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/users/{user_id}/sysadmin:
|
||||
put:
|
||||
summary: Update a registered user to change to be an administrator of Harbor.
|
||||
tags:
|
||||
- users
|
||||
operationId: setUserSysAdmin
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
- name: sysadmin_flag
|
||||
in: body
|
||||
description: Toggle a user to admin or not.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/UserSysAdminFlag'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/200'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/users/{user_id}/password':
|
||||
put:
|
||||
summary: Change the password on a user that already exists.
|
||||
description: |
|
||||
This endpoint is for user to update password. Users with the admin role can change any user's password. Regular users can change only their own password.
|
||||
tags:
|
||||
- users
|
||||
operationId: updateUserPassword
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
- name: password
|
||||
in: body
|
||||
description: Password to be updated, the attribute 'old_password' is optional when the API is called by the system administrator.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PasswordReq'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/200'
|
||||
'400':
|
||||
description: Invalid user ID; Password does not meet requirement
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
description: The caller does not have permission to update the password of the user with given ID, or the old password in request body is not correct.
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/users/current/permissions:
|
||||
get:
|
||||
summary: Get current user permissions.
|
||||
tags:
|
||||
- users
|
||||
operationId: getCurrentUserPermissions
|
||||
parameters:
|
||||
- name: scope
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: The scope for the permission
|
||||
- name: relative
|
||||
in: query
|
||||
type: boolean
|
||||
required: false
|
||||
description: |
|
||||
If true, the resources in the response are relative to the scope,
|
||||
eg for resource '/project/1/repository' if relative is 'true' then the resource in response will be 'repository'.
|
||||
responses:
|
||||
'200':
|
||||
description: Get current user permission successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Permission'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'500':
|
||||
description: Internal errors.
|
||||
'/users/{user_id}/cli_secret':
|
||||
put:
|
||||
summary: Set CLI secret for a user.
|
||||
description: >-
|
||||
This endpoint let user generate a new CLI secret for himself. This API only works when auth mode is set to 'OIDC'.
|
||||
Once this API returns with successful status, the old secret will be invalid, as there will be only one CLI secret
|
||||
for a user.
|
||||
tags:
|
||||
- users
|
||||
operationId: setCliSecret
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: User ID
|
||||
- name: secret
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/OIDCCliSecretReq'
|
||||
responses:
|
||||
'200':
|
||||
description: The secret is successfully updated
|
||||
'400':
|
||||
description: Invalid user ID. Or user is not onboarded via OIDC authentication. Or the secret does not meet the standard.
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'412':
|
||||
description: The auth mode of the system is not "oidc_auth", or the user is not onboarded via OIDC AuthN.
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
parameters:
|
||||
query:
|
||||
name: q
|
||||
@ -4464,7 +4783,6 @@ parameters:
|
||||
required: true
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
@ -7074,3 +7392,131 @@ definitions:
|
||||
editable:
|
||||
type: boolean
|
||||
description: The configure item can be updated or not
|
||||
UserProfile:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
realname:
|
||||
type: string
|
||||
comment:
|
||||
type: string
|
||||
UserCreationReq:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
realname:
|
||||
type: string
|
||||
comment:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
OIDCUserInfo:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int
|
||||
description: the ID of the OIDC info record
|
||||
user_id:
|
||||
type: integer
|
||||
format: int
|
||||
description: the ID of the user
|
||||
subiss:
|
||||
type: string
|
||||
description: the concatenation of sub and issuer in the ID token
|
||||
secret:
|
||||
type: string
|
||||
description: the secret of the OIDC user that can be used for CLI to push/pull artifacts
|
||||
creation_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The creation time of the OIDC user info record.
|
||||
update_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The update time of the OIDC user info record.
|
||||
UserResp:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
realname:
|
||||
type: string
|
||||
comment:
|
||||
type: string
|
||||
user_id:
|
||||
type: integer
|
||||
format: int
|
||||
username:
|
||||
type: string
|
||||
sysadmin_flag:
|
||||
type: boolean
|
||||
x-omitempty: false
|
||||
admin_role_in_auth:
|
||||
type: boolean
|
||||
x-omitempty: false
|
||||
description: indicate the admin privilege is grant by authenticator (LDAP), is always false unless it is the current login user
|
||||
oidc_user_meta:
|
||||
$ref: '#/definitions/OIDCUserInfo'
|
||||
creation_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The creation time of the user.
|
||||
update_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The update time of the user.
|
||||
UserSysAdminFlag:
|
||||
type: object
|
||||
properties:
|
||||
sysadmin_flag:
|
||||
type: boolean
|
||||
description: 'true-admin, false-not admin.'
|
||||
UserSearch:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: integer
|
||||
format: int
|
||||
description: The ID of the user.
|
||||
username:
|
||||
type: string
|
||||
PasswordReq:
|
||||
type: object
|
||||
properties:
|
||||
old_password:
|
||||
type: string
|
||||
description: The user's existing password.
|
||||
new_password:
|
||||
type: string
|
||||
description: New password for marking as to be updated.
|
||||
UserSearchRespItem:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: integer
|
||||
format: int
|
||||
description: The ID of the user.
|
||||
username:
|
||||
type: string
|
||||
Permission:
|
||||
type: object
|
||||
properties:
|
||||
resource:
|
||||
type: string
|
||||
description: The permission resoruce
|
||||
action:
|
||||
type: string
|
||||
description: The permission action
|
||||
OIDCCliSecretReq:
|
||||
type: object
|
||||
properties:
|
||||
secret:
|
||||
type: string
|
||||
description: The new secret
|
||||
|
||||
|
||||
|
@ -26,6 +26,9 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
)
|
||||
|
||||
// ContextName the name of the security context.
|
||||
const ContextName = "local"
|
||||
|
||||
// SecurityContext implements security.Context interface based on database
|
||||
type SecurityContext struct {
|
||||
user *models.User
|
||||
@ -44,7 +47,7 @@ func NewSecurityContext(user *models.User) *SecurityContext {
|
||||
|
||||
// Name returns the name of the security context
|
||||
func (s *SecurityContext) Name() string {
|
||||
return "local"
|
||||
return ContextName
|
||||
}
|
||||
|
||||
// IsAuthenticated returns true if the user has been authenticated
|
||||
|
@ -17,6 +17,7 @@ package project
|
||||
import (
|
||||
"context"
|
||||
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
event "github.com/goharbor/harbor/src/controller/event/metadata"
|
||||
"github.com/goharbor/harbor/src/controller/event/operator"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
@ -61,7 +62,7 @@ type Controller interface {
|
||||
// Update update the project
|
||||
Update(ctx context.Context, project *models.Project) error
|
||||
// ListRoles lists the roles of user for the specific project
|
||||
ListRoles(ctx context.Context, projectID int64, u *user.User) ([]int, error)
|
||||
ListRoles(ctx context.Context, projectID int64, u *commonmodels.User) ([]int, error)
|
||||
}
|
||||
|
||||
// NewController creates an instance of the default project controller
|
||||
@ -241,7 +242,7 @@ func (c *controller) Update(ctx context.Context, p *models.Project) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) ListRoles(ctx context.Context, projectID int64, u *user.User) ([]int, error) {
|
||||
func (c *controller) ListRoles(ctx context.Context, projectID int64, u *commonmodels.User) ([]int, error) {
|
||||
if u == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
134
src/controller/user/controller.go
Normal file
134
src/controller/user/controller.go
Normal file
@ -0,0 +1,134 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/oidc"
|
||||
"github.com/goharbor/harbor/src/pkg/user"
|
||||
"github.com/goharbor/harbor/src/pkg/user/models"
|
||||
)
|
||||
|
||||
var (
|
||||
// Ctl is a global user controller instance
|
||||
Ctl = NewController()
|
||||
)
|
||||
|
||||
// Controller provides functions to support API/middleware for user management and query
|
||||
type Controller interface {
|
||||
// SetSysAdmin ...
|
||||
SetSysAdmin(ctx context.Context, id int, adminFlag bool) error
|
||||
// VerifyPassword ...
|
||||
VerifyPassword(ctx context.Context, username string, password string) (bool, error)
|
||||
// UpdatePassword ...
|
||||
UpdatePassword(ctx context.Context, id int, password string) error
|
||||
// List ...
|
||||
List(ctx context.Context, query *q.Query) ([]*models.User, error)
|
||||
// Create ...
|
||||
Create(ctx context.Context, u *models.User) (int, error)
|
||||
// Count ...
|
||||
Count(ctx context.Context, query *q.Query) (int64, error)
|
||||
// Get ...
|
||||
Get(ctx context.Context, id int, opt *Option) (*models.User, error)
|
||||
// Delete ...
|
||||
Delete(ctx context.Context, id int) error
|
||||
// UpdateProfile update the profile based on the ID and data in the model in parm, only a subset of attributes in the model
|
||||
// will be update, see the implementation of manager.
|
||||
UpdateProfile(ctx context.Context, u *models.User) error
|
||||
// SetCliSecret sets the OIDC CLI secret for a user
|
||||
SetCliSecret(ctx context.Context, id int, secret string) error
|
||||
}
|
||||
|
||||
// NewController ...
|
||||
func NewController() Controller {
|
||||
return &controller{
|
||||
mgr: user.New(),
|
||||
oidcMetaMgr: oidc.NewMetaMgr(),
|
||||
}
|
||||
}
|
||||
|
||||
// Option option for getting User info
|
||||
type Option struct {
|
||||
WithOIDCInfo bool
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
mgr user.Manager
|
||||
oidcMetaMgr oidc.MetaManager
|
||||
}
|
||||
|
||||
func (c *controller) SetCliSecret(ctx context.Context, id int, secret string) error {
|
||||
return c.oidcMetaMgr.SetCliSecretByUserID(ctx, id, secret)
|
||||
}
|
||||
|
||||
func (c *controller) Create(ctx context.Context, u *models.User) (int, error) {
|
||||
return c.mgr.Create(ctx, u)
|
||||
}
|
||||
|
||||
func (c *controller) UpdateProfile(ctx context.Context, u *models.User) error {
|
||||
return c.mgr.UpdateProfile(ctx, u)
|
||||
}
|
||||
|
||||
func (c *controller) Get(ctx context.Context, id int, opt *Option) (*models.User, error) {
|
||||
u, err := c.mgr.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sctx, ok := security.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find security context")
|
||||
}
|
||||
lsc, ok := sctx.(*local.SecurityContext)
|
||||
if ok && lsc.User().UserID == id {
|
||||
u.AdminRoleInAuth = lsc.User().AdminRoleInAuth
|
||||
}
|
||||
if opt != nil && opt.WithOIDCInfo {
|
||||
oidcMeta, err := c.oidcMetaMgr.GetByUserID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.OIDCUserMeta = oidcMeta
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (c *controller) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
return c.mgr.Count(ctx, query)
|
||||
}
|
||||
|
||||
func (c *controller) Delete(ctx context.Context, id int) error {
|
||||
return c.mgr.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (c *controller) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
|
||||
return c.mgr.List(ctx, query)
|
||||
}
|
||||
|
||||
func (c *controller) UpdatePassword(ctx context.Context, id int, password string) error {
|
||||
return c.mgr.UpdatePassword(ctx, id, password)
|
||||
}
|
||||
|
||||
func (c *controller) VerifyPassword(ctx context.Context, username, password string) (bool, error) {
|
||||
return c.mgr.VerifyLocalPassword(ctx, username, password)
|
||||
}
|
||||
|
||||
func (c *controller) SetSysAdmin(ctx context.Context, id int, adminFlag bool) error {
|
||||
return c.mgr.SetSysAdminFlag(ctx, id, adminFlag)
|
||||
}
|
@ -94,18 +94,11 @@ func init() {
|
||||
beego.TestBeegoInit(apppath)
|
||||
|
||||
beego.Router("/api/health", &HealthAPI{}, "get:CheckHealth")
|
||||
beego.Router("/api/users/:id", &UserAPI{}, "get:Get")
|
||||
beego.Router("/api/users", &UserAPI{}, "get:List;post:Post;delete:Delete;put:Put")
|
||||
beego.Router("/api/users/search", &UserAPI{}, "get:Search")
|
||||
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
|
||||
beego.Router("/api/users/:id/permissions", &UserAPI{}, "get:ListUserPermissions")
|
||||
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
|
||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &MetadataAPI{}, "get:Get")
|
||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post")
|
||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &MetadataAPI{}, "put:Put;delete:Delete")
|
||||
beego.Router("/api/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &ProjectMemberAPI{})
|
||||
beego.Router("/api/statistics", &StatisticAPI{})
|
||||
beego.Router("/api/users/?:id", &UserAPI{})
|
||||
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
|
||||
beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List")
|
||||
beego.Router("/api/labels/:id([0-9]+", &LabelAPI{}, "get:Get;put:Put;delete:Delete")
|
||||
|
@ -1,651 +0,0 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/rbac/system"
|
||||
"github.com/goharbor/harbor/src/controller/config"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
)
|
||||
|
||||
// UserAPI handles request to /api/users/{}
|
||||
type UserAPI struct {
|
||||
BaseController
|
||||
currentUserID int
|
||||
userID int
|
||||
SelfRegistration bool
|
||||
AuthMode string
|
||||
secretKey string
|
||||
resource types.Resource
|
||||
}
|
||||
|
||||
type passwordReq struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
type userSearch struct {
|
||||
UserID int `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type secretReq struct {
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// Prepare validates the URL and parms
|
||||
func (ua *UserAPI) Prepare() {
|
||||
ua.BaseController.Prepare()
|
||||
mode, err := config.AuthMode(orm.Context())
|
||||
if err != nil {
|
||||
log.Errorf("failed to get auth mode: %v", err)
|
||||
ua.SendInternalServerError(errors.New(""))
|
||||
return
|
||||
}
|
||||
|
||||
ua.AuthMode = mode
|
||||
if mode == common.OIDCAuth {
|
||||
key, err := config.SecretKey()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get secret key: %v", err)
|
||||
ua.SendInternalServerError(fmt.Errorf("failed to get secret key: %v", err))
|
||||
return
|
||||
}
|
||||
ua.secretKey = key
|
||||
}
|
||||
|
||||
self, err := config.SelfRegistration(orm.Context())
|
||||
if err != nil {
|
||||
log.Errorf("failed to get self registration: %v", err)
|
||||
ua.SendInternalServerError(errors.New(""))
|
||||
return
|
||||
}
|
||||
|
||||
ua.SelfRegistration = self
|
||||
|
||||
if !ua.SecurityCtx.IsAuthenticated() {
|
||||
if ua.Ctx.Input.IsPost() && ua.SelfRegistration {
|
||||
return
|
||||
}
|
||||
ua.SendUnAuthorizedError(errors.New("UnAuthorize"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := dao.GetUser(models.User{
|
||||
Username: ua.SecurityCtx.GetUsername(),
|
||||
})
|
||||
if err != nil {
|
||||
ua.SendInternalServerError(fmt.Errorf("failed to get user %s: %v",
|
||||
ua.SecurityCtx.GetUsername(), err))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
log.Errorf("User with username %s does not exist in DB.", ua.SecurityCtx.GetUsername())
|
||||
ua.SendInternalServerError(fmt.Errorf("user %s does not exist in DB", ua.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
|
||||
ua.currentUserID = user.UserID
|
||||
id := ua.Ctx.Input.Param(":id")
|
||||
if id == "current" {
|
||||
ua.userID = ua.currentUserID
|
||||
} else if len(id) > 0 {
|
||||
var err error
|
||||
ua.userID, err = strconv.Atoi(id)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid user id, error: %v", err)
|
||||
ua.SendBadRequestError(errors.New("invalid user Id"))
|
||||
return
|
||||
}
|
||||
userQuery := models.User{UserID: ua.userID}
|
||||
u, err := dao.GetUser(userQuery)
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in GetUser, error: %v", err)
|
||||
ua.SendInternalServerError(errors.New("internal error"))
|
||||
return
|
||||
}
|
||||
if u == nil {
|
||||
log.Errorf("User with Id: %d does not exist", ua.userID)
|
||||
ua.SendNotFoundError(errors.New(""))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ua.resource = system.NewNamespace().Resource(rbac.ResourceUser)
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (ua *UserAPI) Get() {
|
||||
if ua.userID == ua.currentUserID || ua.SecurityCtx.Can(ua.Context(), rbac.ActionRead, ua.resource) {
|
||||
userQuery := models.User{UserID: ua.userID}
|
||||
u, err := dao.GetUser(userQuery)
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in GetUser, error: %v", err)
|
||||
ua.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
u.Password = ""
|
||||
if ua.userID == ua.currentUserID {
|
||||
sc := ua.SecurityCtx
|
||||
switch lsc := sc.(type) {
|
||||
case *local.SecurityContext:
|
||||
u.AdminRoleInAuth = lsc.User().AdminRoleInAuth
|
||||
}
|
||||
}
|
||||
if ua.AuthMode == common.OIDCAuth {
|
||||
o, err := ua.getOIDCUserInfo()
|
||||
if err != nil {
|
||||
ua.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
u.OIDCUserMeta = o
|
||||
}
|
||||
ua.Data["json"] = u
|
||||
ua.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("Current user, id: %d does not have admin role, can not view other user's detail", ua.currentUserID)
|
||||
ua.SendForbiddenError(errors.New("user does not have admin role"))
|
||||
return
|
||||
}
|
||||
|
||||
// List ...
|
||||
func (ua *UserAPI) List() {
|
||||
if !ua.SecurityCtx.Can(ua.Context(), rbac.ActionList, ua.resource) {
|
||||
log.Errorf("Current user, id: %d does not have admin role, can not list users", ua.currentUserID)
|
||||
ua.SendForbiddenError(errors.New("user does not have admin role"))
|
||||
return
|
||||
}
|
||||
|
||||
page, size, err := ua.GetPaginationParams()
|
||||
if err != nil {
|
||||
ua.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
query := &models.UserQuery{
|
||||
Username: ua.GetString("username"),
|
||||
Email: ua.GetString("email"),
|
||||
Pagination: &models.Pagination{
|
||||
Page: page,
|
||||
Size: size,
|
||||
},
|
||||
}
|
||||
|
||||
total, err := dao.GetTotalOfUsers(query)
|
||||
if err != nil {
|
||||
ua.SendInternalServerError(fmt.Errorf("failed to get total of users: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
users, err := dao.ListUsers(query)
|
||||
if err != nil {
|
||||
ua.SendInternalServerError(fmt.Errorf("failed to get users: %v", err))
|
||||
return
|
||||
}
|
||||
for i := range users {
|
||||
user := &users[i]
|
||||
user.Password = ""
|
||||
}
|
||||
ua.SetPaginationHeader(total, page, size)
|
||||
ua.Data["json"] = users
|
||||
ua.ServeJSON()
|
||||
}
|
||||
|
||||
// Search ...
|
||||
func (ua *UserAPI) Search() {
|
||||
page, size, err := ua.GetPaginationParams()
|
||||
if err != nil {
|
||||
ua.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
query := &models.UserQuery{
|
||||
Username: ua.GetString("username"),
|
||||
Pagination: &models.Pagination{
|
||||
Page: page,
|
||||
Size: size,
|
||||
},
|
||||
}
|
||||
if len(query.Username) == 0 {
|
||||
ua.SendBadRequestError(errors.New("username is required"))
|
||||
return
|
||||
}
|
||||
|
||||
total, err := dao.GetTotalOfUsers(query)
|
||||
if err != nil {
|
||||
ua.SendInternalServerError(fmt.Errorf("failed to get total of users: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
users, err := dao.ListUsers(query)
|
||||
if err != nil {
|
||||
ua.SendInternalServerError(fmt.Errorf("failed to get users: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
var userSearches []userSearch
|
||||
for _, user := range users {
|
||||
userSearches = append(userSearches, userSearch{UserID: user.UserID, Username: user.Username})
|
||||
}
|
||||
|
||||
ua.SetPaginationHeader(total, page, size)
|
||||
ua.Data["json"] = userSearches
|
||||
ua.ServeJSON()
|
||||
}
|
||||
|
||||
// Put ...
|
||||
func (ua *UserAPI) Put() {
|
||||
if !ua.modifiable(ua.Context()) {
|
||||
ua.SendForbiddenError(fmt.Errorf("User with ID %d cannot be modified", ua.userID))
|
||||
return
|
||||
}
|
||||
user := models.User{}
|
||||
if err := ua.DecodeJSONReq(&user); err != nil {
|
||||
ua.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
user.UserID = ua.userID
|
||||
err := commonValidate(user)
|
||||
if err != nil {
|
||||
log.Warningf("Bad request in change user profile: %v", err)
|
||||
ua.SendBadRequestError(fmt.Errorf("change user profile error:" + err.Error()))
|
||||
return
|
||||
}
|
||||
userQuery := models.User{UserID: ua.userID}
|
||||
u, err := dao.GetUser(userQuery)
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in GetUser, error: %v", err)
|
||||
ua.SendInternalServerError(errors.New("internal error"))
|
||||
return
|
||||
}
|
||||
if u == nil {
|
||||
log.Errorf("User with Id: %d does not exist", ua.userID)
|
||||
ua.SendNotFoundError(errors.New(""))
|
||||
return
|
||||
}
|
||||
if u.Email != user.Email {
|
||||
emailExist, err := dao.UserExists(user, "email")
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in change user profile: %v", err)
|
||||
ua.SendInternalServerError(errors.New("internal error"))
|
||||
return
|
||||
}
|
||||
if emailExist {
|
||||
log.Warning("email has already been used!")
|
||||
ua.SendConflictError(errors.New("email has already been used"))
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := dao.ChangeUserProfile(user); err != nil {
|
||||
log.Errorf("Failed to update user profile, error: %v", err)
|
||||
ua.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Post ...
|
||||
func (ua *UserAPI) Post() {
|
||||
|
||||
if !(ua.AuthMode == common.DBAuth) {
|
||||
ua.SendForbiddenError(errors.New(""))
|
||||
return
|
||||
}
|
||||
|
||||
if !(ua.SelfRegistration || ua.SecurityCtx.Can(ua.Context(), rbac.ActionCreate, ua.resource)) {
|
||||
log.Warning("Registration can only be used by admin role user when self-registration is off.")
|
||||
ua.SendForbiddenError(errors.New(""))
|
||||
return
|
||||
}
|
||||
|
||||
if !ua.SecurityCtx.Can(ua.Context(), rbac.ActionCreate, ua.resource) && !lib.GetCarrySession(ua.Ctx.Request.Context()) {
|
||||
ua.SendForbiddenError(errors.New("self-registration cannot be triggered via API"))
|
||||
return
|
||||
}
|
||||
|
||||
user := models.User{}
|
||||
if err := ua.DecodeJSONReq(&user); err != nil {
|
||||
ua.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
err := validate(user)
|
||||
if err != nil {
|
||||
log.Warningf("Bad request in Register: %v", err)
|
||||
ua.RenderError(http.StatusBadRequest, "register error:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !ua.SecurityCtx.Can(ua.Context(), rbac.ActionCreate, ua.resource) && user.SysAdminFlag {
|
||||
msg := "Non-admin cannot create an admin user."
|
||||
log.Errorf(msg)
|
||||
ua.SendForbiddenError(errors.New(msg))
|
||||
return
|
||||
}
|
||||
|
||||
userExist, err := dao.UserExists(user, "username")
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in Register: %v", err)
|
||||
ua.SendInternalServerError(errors.New("internal error"))
|
||||
return
|
||||
}
|
||||
if userExist {
|
||||
log.Warning("username has already been used!")
|
||||
ua.SendConflictError(errors.New("username has already been used"))
|
||||
return
|
||||
}
|
||||
emailExist, err := dao.UserExists(user, "email")
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in change user profile: %v", err)
|
||||
ua.SendInternalServerError(errors.New("internal error"))
|
||||
return
|
||||
}
|
||||
if emailExist {
|
||||
log.Warning("email has already been used!")
|
||||
ua.SendConflictError(errors.New("email has already been used"))
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := dao.Register(user)
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in Register: %v", err)
|
||||
ua.SendInternalServerError(errors.New("internal error"))
|
||||
return
|
||||
}
|
||||
|
||||
ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10))
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (ua *UserAPI) Delete() {
|
||||
if !ua.SecurityCtx.Can(ua.Context(), rbac.ActionDelete, ua.resource) || ua.AuthMode != common.DBAuth || ua.userID == 1 || ua.currentUserID == ua.userID {
|
||||
ua.SendForbiddenError(fmt.Errorf("User with ID: %d cannot be removed, auth mode: %s, current user ID: %d", ua.userID, ua.AuthMode, ua.currentUserID))
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
err = dao.DeleteUser(ua.userID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to delete data from database, error: %v", err)
|
||||
ua.SendInternalServerError(errors.New("failed to delete User"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ChangePassword handles PUT to /api/users/{}/password
|
||||
func (ua *UserAPI) ChangePassword() {
|
||||
if !ua.modifiable(ua.Context()) {
|
||||
ua.SendForbiddenError(fmt.Errorf("User with ID: %d is not modifiable", ua.userID))
|
||||
return
|
||||
}
|
||||
|
||||
changePwdOfOwn := ua.userID == ua.currentUserID
|
||||
|
||||
var req passwordReq
|
||||
if err := ua.DecodeJSONReq(&req); err != nil {
|
||||
ua.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if changePwdOfOwn && len(req.OldPassword) == 0 {
|
||||
ua.SendBadRequestError(errors.New("empty old_password"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateSecret(req.NewPassword); err != nil {
|
||||
ua.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := dao.GetUser(models.User{UserID: ua.userID})
|
||||
if err != nil {
|
||||
ua.SendInternalServerError(fmt.Errorf("failed to get user %d: %v", ua.userID, err))
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
ua.SendNotFoundError(fmt.Errorf("user %d not found", ua.userID))
|
||||
return
|
||||
}
|
||||
if changePwdOfOwn {
|
||||
if user.Password != utils.Encrypt(req.OldPassword, user.Salt, user.PasswordVersion) {
|
||||
log.Info("incorrect old_password")
|
||||
ua.SendForbiddenError(errors.New("incorrect old_password"))
|
||||
return
|
||||
}
|
||||
}
|
||||
if user.Password == utils.Encrypt(req.NewPassword, user.Salt, user.PasswordVersion) {
|
||||
ua.SendBadRequestError(errors.New("the new password can not be same with the old one"))
|
||||
return
|
||||
}
|
||||
|
||||
updatedUser := models.User{
|
||||
UserID: ua.userID,
|
||||
Password: req.NewPassword,
|
||||
PasswordVersion: user.PasswordVersion,
|
||||
}
|
||||
if err = dao.ChangeUserPassword(updatedUser); err != nil {
|
||||
ua.SendInternalServerError(fmt.Errorf("failed to change password of user %d: %v", ua.userID, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ToggleUserAdminRole handles PUT api/users/{}/sysadmin
|
||||
func (ua *UserAPI) ToggleUserAdminRole() {
|
||||
if !ua.SecurityCtx.Can(ua.Context(), rbac.ActionUpdate, ua.resource) {
|
||||
log.Warningf("current user, id: %d does not have admin role, can not update other user's role", ua.currentUserID)
|
||||
ua.RenderError(http.StatusForbidden, "User does not have admin role")
|
||||
return
|
||||
}
|
||||
userQuery := models.User{UserID: ua.userID}
|
||||
if err := ua.DecodeJSONReq(&userQuery); err != nil {
|
||||
ua.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
if err := dao.ToggleUserAdminRole(userQuery.UserID, userQuery.SysAdminFlag); err != nil {
|
||||
log.Errorf("Error occurred in ToggleUserAdminRole: %v", err)
|
||||
ua.SendInternalServerError(errors.New("internal error"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ListUserPermissions handles GET to /api/users/{}/permissions
|
||||
func (ua *UserAPI) ListUserPermissions() {
|
||||
if ua.userID != ua.currentUserID {
|
||||
log.Warningf("Current user, id: %d can not view other user's permissions", ua.currentUserID)
|
||||
ua.RenderError(http.StatusForbidden, "User does not have permission")
|
||||
return
|
||||
}
|
||||
|
||||
relative := ua.Ctx.Input.Query("relative") == "true"
|
||||
|
||||
scope := rbac.Resource(ua.Ctx.Input.Query("scope"))
|
||||
policies := []*types.Policy{}
|
||||
|
||||
ctx := ua.Ctx.Request.Context()
|
||||
if ns, ok := types.NamespaceFromResource(scope); ok {
|
||||
for _, policy := range ns.GetPolicies() {
|
||||
if ua.SecurityCtx.Can(ctx, policy.Action, policy.Resource) {
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := []map[string]string{}
|
||||
for _, policy := range policies {
|
||||
var resource rbac.Resource
|
||||
|
||||
// for resource `/project/1/repository` if `relative` is `true` then the resource in response will be `repository`
|
||||
if relative {
|
||||
relativeResource, err := policy.Resource.RelativeTo(scope)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resource = relativeResource
|
||||
} else {
|
||||
resource = policy.Resource
|
||||
}
|
||||
|
||||
results = append(results, map[string]string{
|
||||
"resource": resource.String(),
|
||||
"action": policy.Action.String(),
|
||||
})
|
||||
}
|
||||
|
||||
ua.Data["json"] = results
|
||||
ua.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// SetCLISecret handles request PUT /api/users/:id/cli_secret to update the CLI secret of the user
|
||||
func (ua *UserAPI) SetCLISecret() {
|
||||
if ua.AuthMode != common.OIDCAuth {
|
||||
ua.SendPreconditionFailedError(errors.New("the auth mode has to be oidc auth"))
|
||||
return
|
||||
}
|
||||
if ua.userID != ua.currentUserID && !ua.SecurityCtx.Can(ua.Context(), rbac.ActionUpdate, ua.resource) {
|
||||
ua.SendForbiddenError(errors.New(""))
|
||||
return
|
||||
}
|
||||
oidcData, err := dao.GetOIDCUserByUserID(ua.userID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get OIDC User meta for user, id: %d, error: %v", ua.userID, err)
|
||||
ua.SendInternalServerError(errors.New("failed to get OIDC meta data for user"))
|
||||
return
|
||||
}
|
||||
if oidcData == nil {
|
||||
log.Errorf("User is not onboarded via OIDC AuthN, user id: %d", ua.userID)
|
||||
ua.SendPreconditionFailedError(errors.New("user is not onboarded via OIDC AuthN"))
|
||||
return
|
||||
}
|
||||
|
||||
s := &secretReq{}
|
||||
if err := ua.DecodeJSONReq(s); err != nil {
|
||||
ua.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
if err := validateSecret(s.Secret); err != nil {
|
||||
ua.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
encSec, err := utils.ReversibleEncrypt(s.Secret, ua.secretKey)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to encrypt secret, error: %v", err)
|
||||
ua.SendInternalServerError(errors.New("failed to encrypt secret"))
|
||||
return
|
||||
}
|
||||
oidcData.Secret = encSec
|
||||
err = dao.UpdateOIDCUserSecret(oidcData)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to update secret in DB, error: %v", err)
|
||||
ua.SendInternalServerError(errors.New("failed to update secret in DB"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (ua *UserAPI) getOIDCUserInfo() (*models.OIDCUser, error) {
|
||||
o, err := dao.GetOIDCUserByUserID(ua.userID)
|
||||
if err != nil || o == nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(o.Secret) > 0 {
|
||||
p, err := utils.ReversibleDecrypt(o.Secret, ua.secretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.PlainSecret = p
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// modifiable returns whether the modify is allowed based on current auth mode and context
|
||||
func (ua *UserAPI) modifiable(ctx context.Context) bool {
|
||||
if ua.AuthMode == common.DBAuth {
|
||||
// When the auth mode is local DB, admin can modify anyone, non-admin can modify himself.
|
||||
return ua.SecurityCtx.Can(ctx, rbac.ActionUpdate, ua.resource) || ua.userID == ua.currentUserID
|
||||
}
|
||||
// When the auth mode is external IDM backend, only the super user can modify himself,
|
||||
// because he's the only one whose information is stored in local DB.
|
||||
return ua.userID == 1 && ua.userID == ua.currentUserID
|
||||
|
||||
}
|
||||
|
||||
// validate only validate when user register
|
||||
func validate(user models.User) error {
|
||||
|
||||
if utils.IsIllegalLength(user.Username, 1, 255) {
|
||||
return fmt.Errorf("username with illegal length")
|
||||
}
|
||||
if utils.IsContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) {
|
||||
return fmt.Errorf("username contains illegal characters")
|
||||
}
|
||||
|
||||
if err := validateSecret(user.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return commonValidate(user)
|
||||
}
|
||||
|
||||
func validateSecret(in string) error {
|
||||
hasLower := regexp.MustCompile(`[a-z]`)
|
||||
hasUpper := regexp.MustCompile(`[A-Z]`)
|
||||
hasNumber := regexp.MustCompile(`[0-9]`)
|
||||
if len(in) >= 8 && hasLower.MatchString(in) && hasUpper.MatchString(in) && hasNumber.MatchString(in) {
|
||||
return nil
|
||||
}
|
||||
return errors.New("the password or secret must longer than 8 chars with at least 1 uppercase letter, 1 lowercase letter and 1 number")
|
||||
}
|
||||
|
||||
// commonValidate validates email, realname, comment information when user register or change their profile
|
||||
func commonValidate(user models.User) error {
|
||||
|
||||
if len(user.Email) > 0 {
|
||||
if m, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, user.Email); !m {
|
||||
return fmt.Errorf("email with illegal format")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Email can't be empty")
|
||||
}
|
||||
|
||||
if utils.IsIllegalLength(user.Realname, 1, 255) {
|
||||
return fmt.Errorf("realname with illegal length")
|
||||
}
|
||||
|
||||
if utils.IsContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) {
|
||||
return fmt.Errorf("realname contains illegal characters")
|
||||
}
|
||||
if utils.IsIllegalLength(user.Comment, -1, 30) {
|
||||
return fmt.Errorf("comment with illegal length")
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
@ -1,702 +0,0 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/controller/config"
|
||||
securitytesting "github.com/goharbor/harbor/src/testing/common/security"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/api"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/testing/apitests/apilib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
)
|
||||
|
||||
var testUser0002ID, testUser0003ID int
|
||||
var testUser0002, testUser0003 apilib.User
|
||||
var testUser0002Auth, testUser0003Auth *usrInfo
|
||||
|
||||
func TestUsersPost(t *testing.T) {
|
||||
|
||||
fmt.Println("Testing User Add")
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
config.Upload(map[string]interface{}{
|
||||
common.AUTHMode: "db_auth",
|
||||
common.SelfRegistration: false,
|
||||
})
|
||||
|
||||
// case 1: register a new user without authentication
|
||||
t.Log("case 1: Register user without authentication")
|
||||
code, err := apiTest.UsersPost(testUser0002)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a test User", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(http.StatusUnauthorized, code, "case 1: Add user status should be 401 for unauthenticated request")
|
||||
}
|
||||
|
||||
config.Upload(map[string]interface{}{
|
||||
common.SelfRegistration: true,
|
||||
})
|
||||
|
||||
// case 2: register a new user without admin auth, expect 403, because self registration is on
|
||||
t.Log("case 2: Register user without admin auth")
|
||||
code, err = apiTest.UsersPost(testUser0002)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a test User", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
// Should be 403 as only admin can call this API, otherwise it has to be called from browser, with session id
|
||||
assert.Equal(http.StatusForbidden, code, "case 2: Add user status should be 403")
|
||||
}
|
||||
|
||||
// case 3: register a new user with admin auth, but username is empty, expect 400
|
||||
t.Log("case 3: Register user with admin auth, but username is empty")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "case 3: Add user status should be 400")
|
||||
}
|
||||
|
||||
// case 4: register a new user with admin auth, but bad username format, expect 400
|
||||
testUser0002.Username = "test@$"
|
||||
t.Log("case 4: Register user with admin auth, but bad username format")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "case 4: Add user status should be 400")
|
||||
}
|
||||
|
||||
// case 5: register a new user with admin auth, but bad userpassword format, expect 400
|
||||
testUser0002.Username = "testUser0002"
|
||||
t.Log("case 5: Register user with admin auth, but empty password.")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "case 5: Add user status should be 400")
|
||||
}
|
||||
|
||||
// case 6: register a new user with admin auth, but email is empty, expect 400
|
||||
testUser0002.Password = "testUser0002"
|
||||
t.Log("case 6: Register user with admin auth, but email is empty")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "case 6: Add user status should be 400")
|
||||
}
|
||||
|
||||
// case 7: register a new user with admin auth, but bad email format, expect 400
|
||||
testUser0002.Email = "test..."
|
||||
t.Log("case 7: Register user with admin auth, but bad email format")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "case 7: Add user status should be 400")
|
||||
}
|
||||
|
||||
// case 7: register a new user with admin auth, but userrealname is empty, expect 400
|
||||
/*
|
||||
testUser0002.Email = "testUser0002@mydomain.com"
|
||||
fmt.Println("Register user with admin auth, but user realname is empty")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "Add user status should be 400")
|
||||
}
|
||||
*/
|
||||
// case 8: register a new user with admin auth, but bad userrealname format, expect 400
|
||||
testUser0002.Email = "testUser0002@mydomain.com"
|
||||
testUser0002.Realname = "test$com"
|
||||
t.Log("case 8: Register user with admin auth, but bad user realname format")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
|
||||
} else {
|
||||
assert.Equal(400, code, "case 8: Add user status should be 400")
|
||||
}
|
||||
|
||||
// case 9: register a new user with admin auth, but bad user comment, expect 400
|
||||
testUser0002.Realname = "testUser0002"
|
||||
testUser0002.Comment = "vmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm"
|
||||
t.Log("case 9: Register user with admin auth, but user comment length is illegal")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "case 9: Add user status should be 400")
|
||||
}
|
||||
testUser0002.Comment = "test user"
|
||||
|
||||
// case 10: register an admin using non-admin user, expect 403
|
||||
t.Log("case 10: Register admin user with non admin auth")
|
||||
testUser0002.HasAdminRole = true
|
||||
code, err = apiTest.UsersPost(testUser0002)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(http.StatusForbidden, code, "case 10: Add user status should be 403")
|
||||
}
|
||||
testUser0002.HasAdminRole = false
|
||||
|
||||
// case 11: register a new user with admin auth, expect 201
|
||||
t.Log("case 11: Register user with admin auth, right parameters")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(201, code, "case 11: Add user status should be 201")
|
||||
}
|
||||
|
||||
// case 12: register duplicate user with admin auth, expect 409
|
||||
t.Log("case 12: Register duplicate user with admin auth")
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(409, code, "case 12: Add user status should be 409")
|
||||
}
|
||||
|
||||
// case 13: register a new user with admin auth, but duplicate email, expect 409
|
||||
t.Log("case 13: Register user with admin auth, but duplicate email")
|
||||
testUser0002.Username = "testUsertest"
|
||||
testUser0002.Email = "testUser0002@mydomain.com"
|
||||
code, err = apiTest.UsersPost(testUser0002, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(409, code, "case 13: Add user status should be 409")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsersGet(t *testing.T) {
|
||||
|
||||
fmt.Println("Testing User Get")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
testUser0002.Username = "testUser0002"
|
||||
// case 1: Get user2 with common auth, but no userid in path, expect 403
|
||||
|
||||
testUser0002Auth = &usrInfo{"testUser0002", "testUser0002"}
|
||||
code, users, err := apiTest.UsersGet(testUser0002.Username, *testUser0002Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while get users", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(403, code, "Get users status should be 403")
|
||||
}
|
||||
// case 2: Get user2 with admin auth, expect 200
|
||||
code, users, err = apiTest.UsersGet(testUser0002.Username, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while get users", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Get users status should be 200")
|
||||
assert.Equal(1, len(users), "Get users record should be 1 ")
|
||||
testUser0002ID = users[0].UserId
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsersSearch(t *testing.T) {
|
||||
|
||||
fmt.Println("Testing User Search")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
testUser0002.Username = "testUser0002"
|
||||
// case 1: Search user2 without auth, expect 401
|
||||
|
||||
testUser0002Auth = &usrInfo{"testUser0002", "testUser0002"}
|
||||
code, users, err := apiTest.UsersSearch(testUser0002.Username)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while search users", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(401, code, "Search users status should be 401")
|
||||
}
|
||||
// case 2: Search user2 with with common auth, expect 200
|
||||
code, users, err = apiTest.UsersSearch(testUser0002.Username, *testUser0002Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while search users", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Search users status should be 200")
|
||||
assert.Equal(1, len(users), "Search users record should be 1 ")
|
||||
testUser0002ID = users[0].UserID
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsersGetByID(t *testing.T) {
|
||||
|
||||
fmt.Println("Testing User GetByID")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
// case 1: Get user2 with userID and his own auth, expect 200
|
||||
code, user, err := apiTest.UsersGetByID(testUser0002ID, *testUser0002Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while get users", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Get users status should be 200")
|
||||
assert.Equal(testUser0002.Username, user.Username, "Get users username should be equal")
|
||||
assert.Equal(testUser0002.Email, user.Email, "Get users email should be equal")
|
||||
}
|
||||
// case 2: Get user2 with user3 auth, expect 403
|
||||
testUser0003.Username = "testUser0003"
|
||||
testUser0003.Email = "testUser0003@mydomain.com"
|
||||
testUser0003.Password = "testUser0003"
|
||||
testUser0003.Realname = "testUser0003"
|
||||
code, err = apiTest.UsersPost(testUser0003, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while add a user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(201, code, "Add user status should be 201")
|
||||
}
|
||||
|
||||
testUser0003Auth = &usrInfo{"testUser0003", "testUser0003"}
|
||||
code, user, err = apiTest.UsersGetByID(testUser0002ID, *testUser0003Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while get users", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(403, code, "Get users status should be 403")
|
||||
}
|
||||
// case 3: Get user that does not exist with user2 auth, expect 404 not found.
|
||||
code, user, err = apiTest.UsersGetByID(1000, *testUser0002Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while get users", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(404, code, "Get users status should be 404")
|
||||
}
|
||||
// Get user3ID in order to delete at the last of the test
|
||||
code, users, err := apiTest.UsersGet(testUser0003.Username, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while get users", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Get users status should be 200")
|
||||
assert.Equal(1, len(users), "Get users record should be 1")
|
||||
testUser0003ID = users[0].UserId
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsersPut(t *testing.T) {
|
||||
fmt.Println("Testing User Put")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
var profile apilib.UserProfile
|
||||
// case 1: change user2 profile with user3 auth
|
||||
code, err := apiTest.UsersPut(testUser0002ID, profile, *testUser0003Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while change user profile", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(403, code, "Change user profile status should be 403")
|
||||
}
|
||||
// case 2: change user3 profile with user2 auth
|
||||
realname := "new realname"
|
||||
email := "new_email@mydomain.com"
|
||||
comment := "new comment"
|
||||
profile.UserID = testUser0003ID
|
||||
profile.Realname = realname
|
||||
profile.Email = email
|
||||
profile.Comment = comment
|
||||
code, err = apiTest.UsersPut(testUser0002ID, profile, *testUser0002Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while change user profile", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Change user profile status should be 200")
|
||||
}
|
||||
_, user, err := apiTest.UsersGetByID(testUser0003ID, *testUser0003Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while get user", err.Error())
|
||||
} else {
|
||||
assert.NotEqual(realname, user.Realname)
|
||||
assert.NotEqual(email, user.Email)
|
||||
assert.NotEqual(comment, user.Comment)
|
||||
}
|
||||
// case 3: change user2 profile with user2 auth, but bad parameters format.
|
||||
profile = apilib.UserProfile{}
|
||||
code, err = apiTest.UsersPut(testUser0002ID, profile, *testUser0002Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while change user profile", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "Change user profile status should be 400")
|
||||
}
|
||||
// case 4: change user2 profile with user2 auth, but duplicate email.
|
||||
profile.Realname = "test user"
|
||||
profile.Email = "testUser0003@mydomain.com"
|
||||
profile.Comment = "change profile"
|
||||
code, err = apiTest.UsersPut(testUser0002ID, profile, *testUser0002Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while change user profile", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(409, code, "Change user profile status should be 409")
|
||||
}
|
||||
// case 5: change user2 profile with user2 auth, right parameters format.
|
||||
profile.Realname = "test user"
|
||||
profile.Email = "testUser0002@vmware.com"
|
||||
profile.Comment = "change profile"
|
||||
code, err = apiTest.UsersPut(testUser0002ID, profile, *testUser0002Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while change user profile", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Change user profile status should be 200")
|
||||
testUser0002.Email = profile.Email
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsersToggleAdminRole(t *testing.T) {
|
||||
fmt.Println("Testing Toggle User Admin Role")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
// case 1: toggle user2 admin role without admin auth
|
||||
code, err := apiTest.UsersToggleAdminRole(testUser0002ID, *testUser0002Auth, true)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while toggle user admin role", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(403, code, "Toggle user admin role status should be 403")
|
||||
}
|
||||
// case 2: toggle user2 admin role with admin auth
|
||||
code, err = apiTest.UsersToggleAdminRole(testUser0002ID, *admin, true)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while toggle user admin role", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Toggle user admin role status should be 200")
|
||||
}
|
||||
}
|
||||
|
||||
func buildChangeUserPasswordURL(id int) string {
|
||||
return fmt.Sprintf("/api/users/%d/password", id)
|
||||
}
|
||||
|
||||
func TestUsersUpdatePassword(t *testing.T) {
|
||||
fmt.Println("Testing Update User Password")
|
||||
oldPassword := "old_Passw0rd"
|
||||
newPassword := "new_Passw0rd"
|
||||
|
||||
user01 := models.User{
|
||||
Username: "user01_for_testing_change_password",
|
||||
Email: "user01_for_testing_change_password@test.com",
|
||||
Password: oldPassword,
|
||||
}
|
||||
id, err := dao.Register(user01)
|
||||
require.Nil(t, err)
|
||||
user01.UserID = int(id)
|
||||
defer dao.DeleteUser(user01.UserID)
|
||||
|
||||
user02 := models.User{
|
||||
Username: "user02_for_testing_change_password",
|
||||
Email: "user02_for_testing_change_password@test.com",
|
||||
Password: oldPassword,
|
||||
}
|
||||
id, err = dao.Register(user02)
|
||||
require.Nil(t, err)
|
||||
user02.UserID = int(id)
|
||||
defer dao.DeleteUser(user02.UserID)
|
||||
|
||||
cases := []*codeCheckingCase{
|
||||
// unauthorized
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: buildChangeUserPasswordURL(user01.UserID),
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 404
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: buildChangeUserPasswordURL(10000),
|
||||
credential: &usrInfo{
|
||||
Name: user01.Username,
|
||||
Passwd: user01.Password,
|
||||
},
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 403, a normal user tries to change password of others
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: buildChangeUserPasswordURL(user02.UserID),
|
||||
credential: &usrInfo{
|
||||
Name: user01.Username,
|
||||
Passwd: user01.Password,
|
||||
},
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 400, empty old password
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: buildChangeUserPasswordURL(user01.UserID),
|
||||
bodyJSON: &passwordReq{},
|
||||
credential: &usrInfo{
|
||||
Name: user01.Username,
|
||||
Passwd: user01.Password,
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 400, empty new password
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: buildChangeUserPasswordURL(user01.UserID),
|
||||
bodyJSON: &passwordReq{
|
||||
OldPassword: oldPassword,
|
||||
},
|
||||
credential: &usrInfo{
|
||||
Name: user01.Username,
|
||||
Passwd: user01.Password,
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 403, incorrect old password
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: buildChangeUserPasswordURL(user01.UserID),
|
||||
bodyJSON: &passwordReq{
|
||||
OldPassword: "incorrect_old_password",
|
||||
NewPassword: newPassword,
|
||||
},
|
||||
credential: &usrInfo{
|
||||
Name: user01.Username,
|
||||
Passwd: user01.Password,
|
||||
},
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 200, normal user change own password
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: buildChangeUserPasswordURL(user01.UserID),
|
||||
bodyJSON: &passwordReq{
|
||||
OldPassword: oldPassword,
|
||||
NewPassword: newPassword,
|
||||
},
|
||||
credential: &usrInfo{
|
||||
Name: user01.Username,
|
||||
Passwd: user01.Password,
|
||||
},
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
// 400, admin user change password of others.
|
||||
// the new password is same with the old one
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: buildChangeUserPasswordURL(user01.UserID),
|
||||
bodyJSON: &passwordReq{
|
||||
NewPassword: newPassword,
|
||||
},
|
||||
credential: admin,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 200, admin user change password of others
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: buildChangeUserPasswordURL(user01.UserID),
|
||||
bodyJSON: &passwordReq{
|
||||
NewPassword: "another_new_Passw0rd",
|
||||
},
|
||||
credential: admin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestUsersDelete(t *testing.T) {
|
||||
|
||||
fmt.Println("Testing User Delete")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
t.Log("delete user-case 1")
|
||||
// case 1:delete user without admin auth
|
||||
code, err := apiTest.UsersDelete(testUser0002ID, *testUser0003Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while delete test user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(403, code, "Delete test user status should be 403")
|
||||
}
|
||||
// case 2: delete user with admin auth, user2 has already been toggled to admin, but can not delete himself
|
||||
t.Log("delete user-case 2")
|
||||
code, err = apiTest.UsersDelete(testUser0002ID, *testUser0002Auth)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while delete test user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(403, code, "Delete test user status should be 403")
|
||||
}
|
||||
// case 3: delete user with admin auth
|
||||
t.Log("delete user-case 3")
|
||||
code, err = apiTest.UsersDelete(testUser0002ID, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while delete test user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Delete test user status should be 200")
|
||||
}
|
||||
// delete user3 with admin auth
|
||||
code, err = apiTest.UsersDelete(testUser0003ID, *admin)
|
||||
if err != nil {
|
||||
t.Error("Error occurred while delete test user", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Delete test user status should be 200")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModifiable(t *testing.T) {
|
||||
t.Log("Test modifiable.")
|
||||
assert := assert.New(t)
|
||||
base := BaseController{
|
||||
BaseAPI: api.BaseAPI{
|
||||
Controller: beego.Controller{},
|
||||
},
|
||||
}
|
||||
|
||||
security := &securitytesting.Context{}
|
||||
security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(false).Once()
|
||||
base.SecurityCtx = security
|
||||
|
||||
ua1 := &UserAPI{
|
||||
BaseController: base,
|
||||
currentUserID: 3,
|
||||
userID: 4,
|
||||
SelfRegistration: false,
|
||||
AuthMode: "db_auth",
|
||||
}
|
||||
assert.False(ua1.modifiable(context.TODO()))
|
||||
|
||||
security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once()
|
||||
ua2 := &UserAPI{
|
||||
BaseController: base,
|
||||
currentUserID: 3,
|
||||
userID: 4,
|
||||
SelfRegistration: false,
|
||||
AuthMode: "db_auth",
|
||||
}
|
||||
assert.True(ua2.modifiable(context.TODO()))
|
||||
|
||||
security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(false).Once()
|
||||
ua3 := &UserAPI{
|
||||
BaseController: base,
|
||||
currentUserID: 3,
|
||||
userID: 4,
|
||||
SelfRegistration: false,
|
||||
AuthMode: "ldap_auth",
|
||||
}
|
||||
assert.False(ua3.modifiable(context.TODO()))
|
||||
|
||||
security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once()
|
||||
ua4 := &UserAPI{
|
||||
BaseController: base,
|
||||
currentUserID: 1,
|
||||
userID: 1,
|
||||
SelfRegistration: false,
|
||||
AuthMode: "ldap_auth",
|
||||
}
|
||||
assert.True(ua4.modifiable(context.TODO()))
|
||||
}
|
||||
|
||||
func TestUsersCurrentPermissions(t *testing.T) {
|
||||
fmt.Println("Testing Get Users Current Permissions")
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
httpStatusCode, permissions, err := apiTest.UsersGetPermissions("current", "/project/1", *projAdmin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
assert.NotEmpty(permissions, "permissions should not be empty")
|
||||
|
||||
httpStatusCode, permissions, err = apiTest.UsersGetPermissions("current", "/unsupport-scope", *projAdmin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
assert.Empty(permissions, "permissions should be empty")
|
||||
|
||||
httpStatusCode, _, err = apiTest.UsersGetPermissions(projAdminID, "/project/1", *projAdmin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
|
||||
httpStatusCode, _, err = apiTest.UsersGetPermissions(projDeveloperID, "/project/1", *projAdmin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(403), httpStatusCode, "httpStatusCode should be 403")
|
||||
}
|
||||
|
||||
func TestValidateSecret(t *testing.T) {
|
||||
assert.NotNil(t, validateSecret(""))
|
||||
assert.NotNil(t, validateSecret("12345678"))
|
||||
assert.NotNil(t, validateSecret("passw0rd"))
|
||||
assert.NotNil(t, validateSecret("PASSW0RD"))
|
||||
assert.NotNil(t, validateSecret("Sh0rt"))
|
||||
assert.Nil(t, validateSecret("Passw0rd"))
|
||||
assert.Nil(t, validateSecret("Thisis1Valid_password"))
|
||||
}
|
@ -56,7 +56,7 @@ func AsNotFoundError(err error, messageFormat string, args ...interface{}) *erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsConflictError checks whether the err is duplicate key error. If it it, wrap it
|
||||
// AsConflictError checks whether the err is duplicate key error. If it is, wrap it
|
||||
// as a src/internal/error.Error with conflict error code, else return nil
|
||||
func AsConflictError(err error, messageFormat string, args ...interface{}) *errors.Error {
|
||||
if IsDuplicateKeyError(err) {
|
||||
|
100
src/pkg/oidc/dao/meta.go
Normal file
100
src/pkg/oidc/dao/meta.go
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
)
|
||||
|
||||
// MetaDAO is the data access object for OIDC user meta
|
||||
type MetaDAO interface {
|
||||
// Create ...
|
||||
Create(ctx context.Context, oidcUser *models.OIDCUser) (int, error)
|
||||
// GetByUsername get the oidc meta record by the user's username
|
||||
GetByUsername(ctx context.Context, username string) (*models.OIDCUser, error)
|
||||
// Update ...
|
||||
Update(ctx context.Context, oidcUser *models.OIDCUser, props ...string) error
|
||||
// List provides a way to query with flexible filter
|
||||
List(ctx context.Context, query *q.Query) ([]*models.OIDCUser, error)
|
||||
}
|
||||
|
||||
// NewMetaDao returns an instance of the default MetaDAO
|
||||
func NewMetaDao() MetaDAO {
|
||||
return &metaDAO{}
|
||||
}
|
||||
|
||||
type metaDAO struct{}
|
||||
|
||||
func (md *metaDAO) GetByUsername(ctx context.Context, username string) (*models.OIDCUser, error) {
|
||||
sql := `SELECT oidc_user.id, oidc_user.user_id, oidc_user.secret, oidc_user.token,
|
||||
oidc_user.creation_time, oidc_user.update_time FROM oidc_user
|
||||
JOIN harbor_user ON oidc_user.user_id = harbor_user.user_id
|
||||
WHERE harbor_user.username = ?`
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &models.OIDCUser{}
|
||||
if err := ormer.Raw(sql, username).QueryRow(res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (md *metaDAO) Update(ctx context.Context, oidcUser *models.OIDCUser, props ...string) error {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := ormer.Update(oidcUser, props...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.NotFoundError(nil).WithMessage("oidc user data with id %d not found", oidcUser.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (md *metaDAO) List(ctx context.Context, query *q.Query) ([]*models.OIDCUser, error) {
|
||||
qs, err := orm.QuerySetter(ctx, &models.OIDCUser{}, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res []*models.OIDCUser
|
||||
if _, err := qs.All(&res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (md *metaDAO) Create(ctx context.Context, oidcUser *models.OIDCUser) (int, error) {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err := ormer.Insert(oidcUser)
|
||||
if e := orm.AsConflictError(err, "The OIDC info for user %d exists, subissuer: %s", oidcUser.UserID, oidcUser.SubIss); e != nil {
|
||||
err = e
|
||||
}
|
||||
return int(id), err
|
||||
}
|
92
src/pkg/oidc/dao/meta_test.go
Normal file
92
src/pkg/oidc/dao/meta_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type MetaDaoTestSuite struct {
|
||||
htesting.Suite
|
||||
dao MetaDAO
|
||||
userID int
|
||||
username string
|
||||
}
|
||||
|
||||
func (suite *MetaDaoTestSuite) SetupSuite() {
|
||||
suite.Suite.SetupSuite()
|
||||
suite.ClearSQLs = []string{}
|
||||
suite.dao = NewMetaDao()
|
||||
suite.userID = 1234
|
||||
suite.username = "oidc_meta_testuser"
|
||||
suite.ExecSQL("INSERT INTO harbor_user (user_id, username,password,realname) VALUES(?,?,'test','test')", suite.userID, suite.username)
|
||||
ctx := orm.Context()
|
||||
_, err := suite.dao.Create(ctx, &models.OIDCUser{
|
||||
UserID: suite.userID,
|
||||
SubIss: `ca4bb144-4b5c-4d1b-9469-69cb3768af8fhttps://sso.andrea.muellerpublic.de/auth/realms/harbor`,
|
||||
Secret: `<enc-v1>7uBP9yqtdnVAhoA243GSv8nOXBWygqzaaEdq9Kqla+q4hOaBZmEMH9vUJi4Yjbh3`,
|
||||
Token: `xxxx`,
|
||||
})
|
||||
suite.Nil(err)
|
||||
suite.appendClearSQL(suite.userID)
|
||||
}
|
||||
|
||||
func (suite *MetaDaoTestSuite) TestList() {
|
||||
ctx := orm.Context()
|
||||
l, err := suite.dao.List(ctx, q.New(q.KeyWords{"user_id": suite.userID}))
|
||||
suite.Nil(err)
|
||||
suite.Equal(1, len(l))
|
||||
suite.Equal("xxxx", l[0].Token)
|
||||
}
|
||||
|
||||
func (suite *MetaDaoTestSuite) TestGetByUsername() {
|
||||
ctx := orm.Context()
|
||||
ou, err := suite.dao.GetByUsername(ctx, suite.username)
|
||||
suite.Nil(err)
|
||||
suite.Equal(suite.userID, ou.UserID)
|
||||
suite.Equal("xxxx", ou.Token)
|
||||
}
|
||||
|
||||
func (suite *MetaDaoTestSuite) TestUpdate() {
|
||||
ctx := orm.Context()
|
||||
l, err := suite.dao.List(ctx, q.New(q.KeyWords{"user_id": suite.userID}))
|
||||
suite.Nil(err)
|
||||
id := l[0].ID
|
||||
ou := &models.OIDCUser{
|
||||
ID: id,
|
||||
Secret: "newsecret",
|
||||
}
|
||||
err = suite.dao.Update(ctx, ou, "secret")
|
||||
suite.Nil(err)
|
||||
l, err = suite.dao.List(ctx, q.New(q.KeyWords{"user_id": suite.userID}))
|
||||
suite.Nil(err)
|
||||
suite.Equal("newsecret", l[0].Secret)
|
||||
}
|
||||
|
||||
func (suite *MetaDaoTestSuite) appendClearSQL(uid int) {
|
||||
suite.ClearSQLs = append(suite.ClearSQLs, fmt.Sprintf("DELETE FROM oidc_user WHERE user_id = %d", uid))
|
||||
suite.ClearSQLs = append(suite.ClearSQLs, fmt.Sprintf("DELETE FROM harbor_user WHERE user_id = %d", uid))
|
||||
}
|
||||
|
||||
func TestMetaDaoTestSuite(t *testing.T) {
|
||||
suite.Run(t, &MetaDaoTestSuite{})
|
||||
}
|
90
src/pkg/oidc/metamanager.go
Normal file
90
src/pkg/oidc/metamanager.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/oidc/dao"
|
||||
)
|
||||
|
||||
// MetaManager is used for managing user's OIDC info
|
||||
type MetaManager interface {
|
||||
// Create creates the oidc user meta record, returns the ID of the record in DB
|
||||
Create(ctx context.Context, oidcUser *models.OIDCUser) (int, error)
|
||||
// GetByUserID gets the oidc meta record by user's ID
|
||||
GetByUserID(ctx context.Context, uid int) (*models.OIDCUser, error)
|
||||
// SetCliSecretByUserID updates the cli secret of a user based on the user ID
|
||||
SetCliSecretByUserID(ctx context.Context, uid int, secret string) error
|
||||
}
|
||||
|
||||
type metaManager struct {
|
||||
dao dao.MetaDAO
|
||||
}
|
||||
|
||||
func (m *metaManager) Create(ctx context.Context, oidcUser *models.OIDCUser) (int, error) {
|
||||
return m.dao.Create(ctx, oidcUser)
|
||||
}
|
||||
|
||||
func (m *metaManager) GetByUserID(ctx context.Context, uid int) (*models.OIDCUser, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
l, err := m.dao.List(ctx, q.New(q.KeyWords{"user_id": uid}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(l) == 0 {
|
||||
return nil, errors.NotFoundError(nil).WithMessage("oidc info for user %d not found", uid)
|
||||
}
|
||||
if len(l) > 1 {
|
||||
logger.Warningf("%d records of oidc user Info found for user %d", len(l), uid)
|
||||
}
|
||||
res := l[0]
|
||||
key, err := keyLoader.encryptKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := utils.ReversibleDecrypt(res.Secret, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.PlainSecret = p
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *metaManager) SetCliSecretByUserID(ctx context.Context, uid int, secret string) error {
|
||||
ou, err := m.GetByUserID(ctx, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := keyLoader.encryptKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := utils.ReversibleEncrypt(secret, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.dao.Update(ctx, &models.OIDCUser{ID: ou.ID, Secret: s}, "secret")
|
||||
}
|
||||
|
||||
// NewMetaMgr returns a default implementation of MetaManager
|
||||
func NewMetaMgr() MetaManager {
|
||||
return &metaManager{dao: dao.NewMetaDao()}
|
||||
}
|
68
src/pkg/oidc/metamanager_test.go
Normal file
68
src/pkg/oidc/metamanager_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
tdao "github.com/goharbor/harbor/src/testing/pkg/oidc/dao"
|
||||
testifymock "github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// encrypt "secret1" using key "naa4JtarA1Zsc3uY" (set in helper_test)
|
||||
var encSecret = "<enc-v1>6FvOrx1O9TKBdalX4gMQrrKNZ99KIyg="
|
||||
|
||||
type metaMgrTestSuite struct {
|
||||
suite.Suite
|
||||
mgr MetaManager
|
||||
dao *tdao.MetaDAO
|
||||
}
|
||||
|
||||
func (m *metaMgrTestSuite) SetupTest() {
|
||||
m.dao = &tdao.MetaDAO{}
|
||||
m.mgr = &metaManager{
|
||||
dao: m.dao,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *metaMgrTestSuite) TestGetByUserID() {
|
||||
{
|
||||
m.dao.On("List", mock.Anything, testifymock.MatchedBy(
|
||||
func(query *q.Query) bool {
|
||||
return query.Keywords["user_id"] == 8
|
||||
})).Return([]*models.OIDCUser{}, nil)
|
||||
_, err := m.mgr.GetByUserID(context.Background(), 8)
|
||||
m.NotNil(err)
|
||||
}
|
||||
{
|
||||
m.dao.On("List", mock.Anything, testifymock.MatchedBy(
|
||||
func(query *q.Query) bool {
|
||||
return query.Keywords["user_id"] == 9
|
||||
})).Return([]*models.OIDCUser{
|
||||
|
||||
{ID: 1, UserID: 9, Secret: encSecret, Token: "token1"},
|
||||
{ID: 2, UserID: 9, Secret: "secret", Token: "token2"},
|
||||
}, nil)
|
||||
ou, err := m.mgr.GetByUserID(context.Background(), 9)
|
||||
m.Nil(err)
|
||||
m.Equal(encSecret, ou.Secret)
|
||||
m.Equal("secret1", ou.PlainSecret)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *metaMgrTestSuite) TestUpdateSecret() {
|
||||
m.dao.On("List", mock.Anything, mock.Anything).Return([]*models.OIDCUser{
|
||||
{ID: 1, UserID: 9, Secret: encSecret, Token: "token1"},
|
||||
}, nil)
|
||||
m.dao.On("Update", mock.Anything, mock.Anything, "secret").Return(nil)
|
||||
err := m.mgr.SetCliSecretByUserID(context.Background(), 9, "new")
|
||||
m.Nil(err)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func TestManager(t *testing.T) {
|
||||
suite.Run(t, &metaMgrTestSuite{})
|
||||
}
|
@ -34,28 +34,37 @@ type SecretManager interface {
|
||||
VerifySecret(ctx context.Context, username string, secret string) (*models.User, error)
|
||||
}
|
||||
|
||||
type defaultManager struct {
|
||||
sync.Mutex
|
||||
type keyGetter struct {
|
||||
sync.RWMutex
|
||||
key string
|
||||
}
|
||||
|
||||
var m SecretManager = &defaultManager{}
|
||||
|
||||
func (dm *defaultManager) getEncryptKey() (string, error) {
|
||||
if dm.key == "" {
|
||||
dm.Lock()
|
||||
defer dm.Unlock()
|
||||
if dm.key == "" {
|
||||
key, err := config.SecretKey()
|
||||
func (kg *keyGetter) encryptKey() (string, error) {
|
||||
kg.RLock()
|
||||
if kg.key == "" {
|
||||
kg.RUnlock()
|
||||
kg.Lock()
|
||||
defer kg.Unlock()
|
||||
if kg.key == "" {
|
||||
k, err := config.SecretKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dm.key = key
|
||||
kg.key = k
|
||||
}
|
||||
} else {
|
||||
defer kg.RUnlock()
|
||||
}
|
||||
return dm.key, nil
|
||||
return kg.key, nil
|
||||
}
|
||||
|
||||
var keyLoader = &keyGetter{}
|
||||
|
||||
type defaultManager struct {
|
||||
}
|
||||
|
||||
var m SecretManager = &defaultManager{}
|
||||
|
||||
// VerifySecret verifies the secret and the token associated with it, it refreshes the token in the DB if it's
|
||||
// refreshed during the verification. It returns a populated user model based on the ID token associated with the secret.
|
||||
func (dm *defaultManager) VerifySecret(ctx context.Context, username string, secret string) (*models.User, error) {
|
||||
@ -74,7 +83,7 @@ func (dm *defaultManager) VerifySecret(ctx context.Context, username string, sec
|
||||
if oidcUser == nil {
|
||||
return nil, fmt.Errorf("user is not onboarded as OIDC user, username: %s", username)
|
||||
}
|
||||
key, err := dm.getEncryptKey()
|
||||
key, err := keyLoader.encryptKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load the key for encryption/decryption: %v", err)
|
||||
}
|
||||
|
@ -15,13 +15,13 @@ func TestSecretVerifyError(t *testing.T) {
|
||||
assert.Equal(t, sve, err)
|
||||
}
|
||||
|
||||
func TestDefaultManagerGetEncryptKey(t *testing.T) {
|
||||
d := &defaultManager{}
|
||||
k, err := d.getEncryptKey()
|
||||
func TestGetEncryptKey(t *testing.T) {
|
||||
kl := &keyGetter{}
|
||||
k, err := kl.encryptKey()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "naa4JtarA1Zsc3uY", k)
|
||||
d2 := &defaultManager{key: "oldkey"}
|
||||
k2, err := d2.getEncryptKey()
|
||||
kl2 := &keyGetter{key: "oldkey"}
|
||||
k2, err := kl2.encryptKey()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "oldkey", k2)
|
||||
}
|
||||
|
@ -16,11 +16,12 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/repository/dao"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type managerTestSuite struct {
|
||||
|
@ -17,6 +17,7 @@ package dao
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/user/models"
|
||||
@ -24,8 +25,14 @@ import (
|
||||
|
||||
// DAO is the data access object interface for user
|
||||
type DAO interface {
|
||||
// Create create a user record in the table, it will return the ID of the user
|
||||
Create(ctx context.Context, user *models.User) (int, error)
|
||||
// List list users
|
||||
List(ctx context.Context, query *q.Query) ([]*models.User, error)
|
||||
// Count counts the number of users
|
||||
Count(ctx context.Context, query *q.Query) (int64, error)
|
||||
// Update updates the user record based on the model the parm props are the columns will be updated
|
||||
Update(ctx context.Context, user *models.User, props ...string) error
|
||||
}
|
||||
|
||||
// New returns an instance of the default DAO
|
||||
@ -33,8 +40,52 @@ func New() DAO {
|
||||
return &dao{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// TODO beegoorm.RegisterModel(new(models.User))
|
||||
}
|
||||
|
||||
type dao struct{}
|
||||
|
||||
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
query = q.MustClone(query)
|
||||
query.Keywords["deleted"] = false
|
||||
qs, err := orm.QuerySetterForCount(ctx, &models.User{}, query)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return qs.Count()
|
||||
}
|
||||
|
||||
func (d *dao) Create(ctx context.Context, user *models.User) (int, error) {
|
||||
if user.UserID > 0 {
|
||||
return 0, errors.BadRequestError(nil).WithMessage("user ID is set when creating user: %d", user.UserID)
|
||||
}
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err := ormer.Insert(user)
|
||||
if err != nil {
|
||||
return 0, orm.WrapConflictError(err, "user %s or email %s already exists", user.Username, user.Email)
|
||||
}
|
||||
return int(id), nil
|
||||
}
|
||||
|
||||
func (d *dao) Update(ctx context.Context, user *models.User, props ...string) error {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := ormer.Update(user, props...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.NotFoundError(nil).WithMessage("user with id %d not found", user.UserID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List list users
|
||||
func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
|
||||
query = q.MustClone(query)
|
||||
|
@ -1,5 +1,4 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@ -15,10 +14,12 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/user/models"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -30,9 +31,48 @@ type DaoTestSuite struct {
|
||||
|
||||
func (suite *DaoTestSuite) SetupSuite() {
|
||||
suite.Suite.SetupSuite()
|
||||
suite.ClearSQLs = []string{}
|
||||
suite.dao = New()
|
||||
}
|
||||
|
||||
func (suite *DaoTestSuite) TestCount() {
|
||||
ctx := orm.Context()
|
||||
{
|
||||
n, err := suite.dao.Count(ctx, nil)
|
||||
suite.Nil(err)
|
||||
users, err := suite.dao.List(orm.Context(), nil)
|
||||
suite.Nil(err)
|
||||
suite.Equal(len(users), int(n))
|
||||
}
|
||||
{
|
||||
|
||||
n, err := suite.dao.Count(ctx, nil)
|
||||
suite.Nil(err)
|
||||
id, err := suite.dao.Create(ctx, &models.User{
|
||||
Username: "testuser2",
|
||||
Realname: "user test",
|
||||
Email: "testuser@test.com",
|
||||
Password: "somepassword",
|
||||
PasswordVersion: "sha256",
|
||||
})
|
||||
suite.Nil(err)
|
||||
defer suite.appendClearSQL(id)
|
||||
n2, err := suite.dao.Count(ctx, nil)
|
||||
suite.Nil(err)
|
||||
suite.Equal(n+1, n2)
|
||||
err2 := suite.dao.Update(ctx, &models.User{
|
||||
UserID: id,
|
||||
Deleted: true,
|
||||
})
|
||||
suite.Nil(err2)
|
||||
n3, err := suite.dao.Count(ctx, nil)
|
||||
suite.Nil(err)
|
||||
suite.Equal(n, n3)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *DaoTestSuite) TestList() {
|
||||
{
|
||||
users, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"user_id": 1}))
|
||||
@ -47,6 +87,61 @@ func (suite *DaoTestSuite) TestList() {
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *DaoTestSuite) TestCreate() {
|
||||
cases := []struct {
|
||||
name string
|
||||
input *models.User
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
name: "create with user ID",
|
||||
input: &models.User{
|
||||
UserID: 3,
|
||||
Username: "testuser",
|
||||
Realname: "user test",
|
||||
Email: "testuser@test.com",
|
||||
Password: "somepassword",
|
||||
PasswordVersion: "sha256",
|
||||
},
|
||||
hasError: true,
|
||||
},
|
||||
{
|
||||
name: "create without user ID",
|
||||
input: &models.User{
|
||||
Username: "testuser",
|
||||
Realname: "user test",
|
||||
Email: "testuser@test.com",
|
||||
Password: "somepassword",
|
||||
PasswordVersion: "sha256",
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
suite.Run(c.name, func() {
|
||||
ctx := orm.Context()
|
||||
id, err := suite.dao.Create(ctx, c.input)
|
||||
defer suite.appendClearSQL(id)
|
||||
if c.hasError {
|
||||
suite.NotNil(err)
|
||||
} else {
|
||||
suite.Nil(err)
|
||||
l, err2 := suite.dao.List(ctx, q.New(q.KeyWords{"user_id": id}))
|
||||
suite.Nil(err2)
|
||||
suite.Equal(c.input.Username, l[0].Username)
|
||||
suite.Equal(c.input.Password, l[0].Password)
|
||||
suite.Equal(c.input.Email, l[0].Email)
|
||||
suite.Equal(c.input.Realname, l[0].Realname)
|
||||
suite.Equal(c.input.PasswordVersion, l[0].PasswordVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *DaoTestSuite) appendClearSQL(uid int) {
|
||||
suite.ClearSQLs = append(suite.ClearSQLs, fmt.Sprintf("DELETE FROM harbor_user WHERE user_id = %d", uid))
|
||||
}
|
||||
|
||||
func TestDaoTestSuite(t *testing.T) {
|
||||
suite.Run(t, &DaoTestSuite{})
|
||||
}
|
||||
|
@ -16,17 +16,16 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/user/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/user/models"
|
||||
)
|
||||
|
||||
// User alias to models.User
|
||||
type User = models.User
|
||||
|
||||
var (
|
||||
// Mgr is the global project manager
|
||||
Mgr = New()
|
||||
@ -40,6 +39,20 @@ type Manager interface {
|
||||
GetByName(ctx context.Context, username string) (*models.User, error)
|
||||
// List users according to the query
|
||||
List(ctx context.Context, query *q.Query) (models.Users, error)
|
||||
// Count counts the number of users according to the query
|
||||
Count(ctx context.Context, query *q.Query) (int64, error)
|
||||
// Create creates the user, the password of input should be plaintext
|
||||
Create(ctx context.Context, user *models.User) (int, error)
|
||||
// Delete deletes the user by updating user's delete flag and update the name and Email
|
||||
Delete(ctx context.Context, id int) error
|
||||
// SetSysAdminFlag sets the system admin flag of the user in local DB
|
||||
SetSysAdminFlag(ctx context.Context, id int, admin bool) error
|
||||
// UpdateProfile updates the user's profile
|
||||
UpdateProfile(ctx context.Context, user *models.User) error
|
||||
// UpdatePassword updates user's password
|
||||
UpdatePassword(ctx context.Context, id int, newPassword string) error
|
||||
// VerifyLocalPassword verifies the password against the record in DB based on the input
|
||||
VerifyLocalPassword(ctx context.Context, username, password string) (bool, error)
|
||||
}
|
||||
|
||||
// New returns a default implementation of Manager
|
||||
@ -51,6 +64,54 @@ type manager struct {
|
||||
dao dao.DAO
|
||||
}
|
||||
|
||||
func (m *manager) Delete(ctx context.Context, id int) error {
|
||||
u, err := m.Get(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Username = fmt.Sprintf("%s#%d", u.Username, u.UserID)
|
||||
u.Email = fmt.Sprintf("%s#%d", u.Email, u.UserID)
|
||||
u.Deleted = true
|
||||
return m.dao.Update(ctx, u, "username", "email", "deleted")
|
||||
}
|
||||
|
||||
func (m *manager) VerifyLocalPassword(ctx context.Context, username, password string) (bool, error) {
|
||||
u, err := m.GetByName(ctx, username)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return utils.Encrypt(password, u.Salt, u.PasswordVersion) == u.Password, nil
|
||||
}
|
||||
|
||||
func (m *manager) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
return m.dao.Count(ctx, query)
|
||||
}
|
||||
|
||||
func (m *manager) UpdateProfile(ctx context.Context, user *models.User) error {
|
||||
return m.dao.Update(ctx, user, "email", "realname", "comment")
|
||||
}
|
||||
|
||||
func (m *manager) UpdatePassword(ctx context.Context, id int, newPassword string) error {
|
||||
user := &models.User{
|
||||
UserID: id,
|
||||
}
|
||||
injectPasswd(user, newPassword)
|
||||
return m.dao.Update(ctx, user, "salt", "password", "password_version")
|
||||
}
|
||||
|
||||
func (m *manager) SetSysAdminFlag(ctx context.Context, id int, admin bool) error {
|
||||
u := &models.User{
|
||||
UserID: id,
|
||||
SysAdminFlag: admin,
|
||||
}
|
||||
return m.dao.Update(ctx, u, "sysadmin_flag")
|
||||
}
|
||||
|
||||
func (m *manager) Create(ctx context.Context, user *models.User) (int, error) {
|
||||
injectPasswd(user, user.Password)
|
||||
return m.dao.Create(ctx, user)
|
||||
}
|
||||
|
||||
// Get get user by user id
|
||||
func (m *manager) Get(ctx context.Context, id int) (*models.User, error) {
|
||||
users, err := m.dao.List(ctx, q.New(q.KeyWords{"user_id": id}))
|
||||
@ -93,11 +154,16 @@ func (m *manager) List(ctx context.Context, query *q.Query) (models.Users, error
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if excludeAdmin {
|
||||
// Exclude admin account when not filter by UserIDs, see https://github.com/goharbor/harbor/issues/2527
|
||||
query.Keywords["user_id__gt"] = 1
|
||||
}
|
||||
|
||||
return m.dao.List(ctx, query)
|
||||
}
|
||||
|
||||
func injectPasswd(u *models.User, password string) {
|
||||
salt := utils.GenerateRandomString()
|
||||
u.Password = utils.Encrypt(password, salt, utils.SHA256)
|
||||
u.Salt = salt
|
||||
u.PasswordVersion = utils.SHA256
|
||||
}
|
||||
|
60
src/pkg/user/manager_test.go
Normal file
60
src/pkg/user/manager_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/user/dao"
|
||||
"github.com/stretchr/testify/assert"
|
||||
testifymock "github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type mgrTestSuite struct {
|
||||
suite.Suite
|
||||
mgr Manager
|
||||
dao *dao.DAO
|
||||
}
|
||||
|
||||
func (m *mgrTestSuite) SetupTest() {
|
||||
m.dao = &dao.DAO{}
|
||||
m.mgr = &manager{
|
||||
dao: m.dao,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mgrTestSuite) TestCount() {
|
||||
m.dao.On("Count", mock.Anything, mock.Anything).Return(int64(1), nil)
|
||||
n, err := m.mgr.Count(context.Background(), nil)
|
||||
m.Nil(err)
|
||||
m.Equal(int64(1), n)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *mgrTestSuite) TestSetAdminFlag() {
|
||||
id := 9
|
||||
m.dao.On("Update", mock.Anything, testifymock.MatchedBy(
|
||||
func(u *models.User) bool {
|
||||
return u.UserID == 9 && u.SysAdminFlag
|
||||
}), "sysadmin_flag").Return(nil)
|
||||
err := m.mgr.SetSysAdminFlag(context.Background(), id, true)
|
||||
m.Nil(err)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func TestManager(t *testing.T) {
|
||||
suite.Run(t, &mgrTestSuite{})
|
||||
}
|
||||
|
||||
func TestInjectPasswd(t *testing.T) {
|
||||
u := &models.User{
|
||||
UserID: 9,
|
||||
}
|
||||
p := "pass"
|
||||
injectPasswd(u, p)
|
||||
assert.Equal(t, "sha256", u.PasswordVersion)
|
||||
assert.Equal(t, utils.Encrypt(p, u.Salt, "sha256"), u.Password)
|
||||
}
|
@ -15,11 +15,13 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
// "time"
|
||||
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
)
|
||||
|
||||
// User ...
|
||||
type User = models.User
|
||||
type User = commonmodels.User
|
||||
|
||||
// Users the collection for User
|
||||
type Users []*User
|
||||
|
@ -58,6 +58,7 @@ func New() http.Handler {
|
||||
SystemCVEAllowlistAPI: newSystemCVEAllowListAPI(),
|
||||
ConfigureAPI: newConfigAPI(),
|
||||
UsergroupAPI: newUserGroupAPI(),
|
||||
UsersAPI: newUsersAPI(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
55
src/server/v2.0/handler/model/user.go
Normal file
55
src/server/v2.0/handler/model/user.go
Normal file
@ -0,0 +1,55 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/goharbor/harbor/src/pkg/user/models"
|
||||
svrmodels "github.com/goharbor/harbor/src/server/v2.0/models"
|
||||
)
|
||||
|
||||
// User ...
|
||||
type User struct {
|
||||
*models.User
|
||||
}
|
||||
|
||||
// ToSearchRespItem ...
|
||||
func (u *User) ToSearchRespItem() *svrmodels.UserSearchRespItem {
|
||||
return &svrmodels.UserSearchRespItem{
|
||||
UserID: int64(u.UserID),
|
||||
Username: u.Username,
|
||||
}
|
||||
}
|
||||
|
||||
// ToUserProfile ...
|
||||
func (u *User) ToUserProfile() *svrmodels.UserProfile {
|
||||
return &svrmodels.UserProfile{
|
||||
Email: u.Email,
|
||||
Realname: u.Realname,
|
||||
Comment: u.Comment,
|
||||
}
|
||||
}
|
||||
|
||||
// ToUserResp ...
|
||||
func (u *User) ToUserResp() *svrmodels.UserResp {
|
||||
res := &svrmodels.UserResp{
|
||||
Email: u.Email,
|
||||
Realname: u.Realname,
|
||||
Comment: u.Comment,
|
||||
UserID: int64(u.UserID),
|
||||
Username: u.Username,
|
||||
SysadminFlag: u.SysAdminFlag,
|
||||
AdminRoleInAuth: u.AdminRoleInAuth,
|
||||
CreationTime: strfmt.DateTime(u.CreationTime),
|
||||
UpdateTime: strfmt.DateTime(u.UpdateTime),
|
||||
}
|
||||
if u.OIDCUserMeta != nil {
|
||||
res.OidcUserMeta = &svrmodels.OIDCUserInfo{
|
||||
ID: u.OIDCUserMeta.ID,
|
||||
UserID: int64(u.OIDCUserMeta.UserID),
|
||||
Subiss: u.OIDCUserMeta.SubIss,
|
||||
Secret: u.OIDCUserMeta.PlainSecret,
|
||||
CreationTime: strfmt.DateTime(u.OIDCUserMeta.CreationTime),
|
||||
UpdateTime: strfmt.DateTime(u.OIDCUserMeta.UpdateTime),
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
479
src/server/v2.0/handler/user.go
Normal file
479
src/server/v2.0/handler/user.go
Normal file
@ -0,0 +1,479 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/rbac/system"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/controller/config"
|
||||
"github.com/goharbor/harbor/src/controller/user"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
usermodels "github.com/goharbor/harbor/src/pkg/user/models"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/restapi/operations/users"
|
||||
)
|
||||
|
||||
var userResource = system.NewNamespace().Resource(rbac.ResourceUser)
|
||||
|
||||
type usersAPI struct {
|
||||
BaseAPI
|
||||
ctl user.Controller
|
||||
getAuth func(ctx context.Context) (string, error) // For testing
|
||||
}
|
||||
|
||||
func newUsersAPI() restapi.UsersAPI {
|
||||
return &usersAPI{
|
||||
ctl: user.Ctl,
|
||||
getAuth: config.AuthMode,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *usersAPI) SetCliSecret(ctx context.Context, params users.SetCliSecretParams) middleware.Responder {
|
||||
uid := int(params.UserID)
|
||||
if err := u.requireForCLISecret(ctx, uid); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if err := requireValidSecret(params.Secret.Secret); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if err := u.ctl.SetCliSecret(ctx, uid, params.Secret.Secret); err != nil {
|
||||
log.G(ctx).Errorf("Failed to set CLI secret, error: %v", err)
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
return users.NewSetCliSecretOK()
|
||||
}
|
||||
|
||||
func (u *usersAPI) CreateUser(ctx context.Context, params users.CreateUserParams) middleware.Responder {
|
||||
if err := u.requireCreatable(ctx); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if err := requireValidSecret(params.UserReq.Password); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
m := &usermodels.User{
|
||||
Username: params.UserReq.Username,
|
||||
Realname: params.UserReq.Realname,
|
||||
Email: params.UserReq.Email,
|
||||
Comment: params.UserReq.Comment,
|
||||
Password: params.UserReq.Password,
|
||||
}
|
||||
if err := validateUserProfile(m); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
uid, err := u.ctl.Create(ctx, m)
|
||||
if err != nil {
|
||||
log.G(ctx).Errorf("Failed to create user, error: %v", err)
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), uid)
|
||||
return users.NewCreateUserCreated().WithLocation(location)
|
||||
|
||||
}
|
||||
|
||||
func (u *usersAPI) ListUsers(ctx context.Context, params users.ListUsersParams) middleware.Responder {
|
||||
if err := u.RequireSystemAccess(ctx, rbac.ActionList, userResource); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
query, err := u.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
values := params.HTTPRequest.URL.Query()
|
||||
for _, k := range []string{"username", "email"} {
|
||||
if v := values.Get(k); v != "" {
|
||||
query.Keywords[k] = &q.FuzzyMatchValue{Value: v}
|
||||
}
|
||||
}
|
||||
total, err := u.ctl.Count(ctx, query)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
payload := make([]*models.UserResp, 0)
|
||||
if total > 0 {
|
||||
ul, err := u.ctl.List(ctx, query)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
payload = make([]*models.UserResp, len(ul))
|
||||
for i, u := range ul {
|
||||
m := &model.User{
|
||||
User: u,
|
||||
}
|
||||
payload[i] = m.ToUserResp()
|
||||
}
|
||||
}
|
||||
return users.NewListUsersOK().
|
||||
WithPayload(payload).
|
||||
WithLink(u.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
|
||||
WithXTotalCount(total)
|
||||
}
|
||||
|
||||
func (u *usersAPI) GetCurrentUserPermissions(ctx context.Context, params users.GetCurrentUserPermissionsParams) middleware.Responder {
|
||||
if err := u.RequireAuthenticated(ctx); err != nil {
|
||||
u.SendError(ctx, err)
|
||||
}
|
||||
scope := ""
|
||||
if params.Scope != nil {
|
||||
scope = *params.Scope
|
||||
}
|
||||
var policies []*types.Policy
|
||||
sctx, _ := security.FromContext(ctx)
|
||||
if ns, ok := types.NamespaceFromResource(rbac.Resource(scope)); ok {
|
||||
for _, policy := range ns.GetPolicies() {
|
||||
if sctx.Can(ctx, policy.Action, policy.Resource) {
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
var res []*models.Permission
|
||||
relative := lib.BoolValue(params.Relative)
|
||||
for _, policy := range policies {
|
||||
var resource rbac.Resource
|
||||
// for resource `/project/1/repository` if `relative` is `true` then the resource in response will be `repository`
|
||||
if relative {
|
||||
relativeResource, err := policy.Resource.RelativeTo(rbac.Resource(scope))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resource = relativeResource
|
||||
} else {
|
||||
resource = policy.Resource
|
||||
}
|
||||
res = append(res, &models.Permission{
|
||||
Resource: resource.String(),
|
||||
Action: policy.Action.String(),
|
||||
})
|
||||
}
|
||||
return users.NewGetCurrentUserPermissionsOK().WithPayload(res)
|
||||
}
|
||||
|
||||
func (u *usersAPI) DeleteUser(ctx context.Context, params users.DeleteUserParams) middleware.Responder {
|
||||
uid := int(params.UserID)
|
||||
if err := u.requireDeletable(ctx, uid); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if err := u.ctl.Delete(ctx, uid); err != nil {
|
||||
log.G(ctx).Errorf("Failed to delete user %d, error: %v", uid, err)
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
return users.NewDeleteUserOK()
|
||||
}
|
||||
|
||||
func (u *usersAPI) GetCurrentUserInfo(ctx context.Context, params users.GetCurrentUserInfoParams) middleware.Responder {
|
||||
if err := u.RequireAuthenticated(ctx); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
sctx, _ := security.FromContext(ctx)
|
||||
lsc, ok := sctx.(*local.SecurityContext)
|
||||
if !ok {
|
||||
return u.SendError(ctx, errors.PreconditionFailedError(nil).WithMessage("get current user not available for security context: %s", sctx.Name()))
|
||||
}
|
||||
resp, err := u.getUserByID(ctx, lsc.User().UserID)
|
||||
if err != nil {
|
||||
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
return users.NewGetCurrentUserInfoOK().WithPayload(resp)
|
||||
}
|
||||
|
||||
func (u *usersAPI) GetUser(ctx context.Context, params users.GetUserParams) middleware.Responder {
|
||||
uid := int(params.UserID)
|
||||
if err := u.requireReadable(ctx, uid); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
resp, err := u.getUserByID(ctx, uid)
|
||||
if err != nil {
|
||||
log.G(ctx).Errorf("Failed to get user info for ID %d, error: %v", uid, err)
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
return users.NewGetUserOK().WithPayload(resp)
|
||||
}
|
||||
|
||||
func (u *usersAPI) getUserByID(ctx context.Context, id int) (*models.UserResp, error) {
|
||||
auth, err := u.getAuth(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opt := &user.Option{
|
||||
WithOIDCInfo: auth == common.OIDCAuth,
|
||||
}
|
||||
|
||||
us, err := u.ctl.Get(ctx, id, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := &model.User{
|
||||
User: us,
|
||||
}
|
||||
return m.ToUserResp(), nil
|
||||
}
|
||||
|
||||
func (u *usersAPI) UpdateUserProfile(ctx context.Context, params users.UpdateUserProfileParams) middleware.Responder {
|
||||
uid := int(params.UserID)
|
||||
if err := u.requireModifiable(ctx, uid); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
m := &usermodels.User{
|
||||
UserID: uid,
|
||||
Realname: params.Profile.Realname,
|
||||
Email: params.Profile.Email,
|
||||
Comment: params.Profile.Comment,
|
||||
}
|
||||
if err := validateUserProfile(m); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if err := u.ctl.UpdateProfile(ctx, m); err != nil {
|
||||
log.G(ctx).Errorf("Failed to update user profile, error: %v", err)
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
return users.NewUpdateUserProfileOK()
|
||||
}
|
||||
|
||||
func (u *usersAPI) SearchUsers(ctx context.Context, params users.SearchUsersParams) middleware.Responder {
|
||||
if err := u.RequireAuthenticated(ctx); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
query, err := u.BuildQuery(ctx, nil, nil, params.Page, params.PageSize)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
query.Keywords["username"] = &q.FuzzyMatchValue{Value: params.Username}
|
||||
total, err := u.ctl.Count(ctx, query)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if total == 0 {
|
||||
return users.NewSearchUsersOK().WithXTotalCount(0).WithPayload([]*models.UserSearchRespItem{})
|
||||
}
|
||||
l, err := u.ctl.List(ctx, query)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
var result []*models.UserSearchRespItem
|
||||
for _, us := range l {
|
||||
m := &model.User{User: us}
|
||||
result = append(result, m.ToSearchRespItem())
|
||||
}
|
||||
return users.NewSearchUsersOK().
|
||||
WithXTotalCount(total).
|
||||
WithPayload(result).
|
||||
WithLink(u.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String())
|
||||
}
|
||||
|
||||
func (u *usersAPI) UpdateUserPassword(ctx context.Context, params users.UpdateUserPasswordParams) middleware.Responder {
|
||||
uid := int(params.UserID)
|
||||
if err := u.requireModifiable(ctx, uid); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
sctx, _ := security.FromContext(ctx)
|
||||
if matchUserID(sctx, uid) {
|
||||
ok, err := u.ctl.VerifyPassword(ctx, sctx.GetUsername(), params.Password.OldPassword)
|
||||
if err != nil {
|
||||
log.G(ctx).Errorf("Failed to verify password for user: %s, error: %v", sctx.GetUsername(), err)
|
||||
return u.SendError(ctx, errors.UnknownError(nil).WithMessage("Failed to verify password"))
|
||||
}
|
||||
if !ok {
|
||||
return u.SendError(ctx, errors.ForbiddenError(nil).WithMessage("Current password is incorrect"))
|
||||
}
|
||||
}
|
||||
newPwd := params.Password.NewPassword
|
||||
if err := requireValidSecret(newPwd); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
ok, err := u.ctl.VerifyPassword(ctx, sctx.GetUsername(), newPwd)
|
||||
if err != nil {
|
||||
log.G(ctx).Errorf("Failed to verify password for user: %s, error: %v", sctx.GetUsername(), err)
|
||||
return u.SendError(ctx, errors.UnknownError(nil).WithMessage("Failed to verify password"))
|
||||
}
|
||||
if ok {
|
||||
return u.SendError(ctx, errors.BadRequestError(nil).WithMessage("New password is identical to old password"))
|
||||
}
|
||||
err2 := u.ctl.UpdatePassword(ctx, uid, params.Password.NewPassword)
|
||||
if err2 != nil {
|
||||
log.G(ctx).Errorf("Failed to update password, error: %v", err)
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
return users.NewUpdateUserPasswordOK()
|
||||
}
|
||||
|
||||
func (u *usersAPI) SetUserSysAdmin(ctx context.Context, params users.SetUserSysAdminParams) middleware.Responder {
|
||||
id := int(params.UserID)
|
||||
if err := u.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceUser); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if err := u.ctl.SetSysAdmin(ctx, id, params.SysadminFlag.SysadminFlag); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
return users.NewSetUserSysAdminOK()
|
||||
}
|
||||
|
||||
func (u *usersAPI) requireForCLISecret(ctx context.Context, id int) error {
|
||||
a, err := u.getAuth(ctx)
|
||||
if err != nil {
|
||||
log.G(ctx).Errorf("Failed to get authmode, error: %v", err)
|
||||
return err
|
||||
}
|
||||
if a != common.OIDCAuth {
|
||||
return errors.PreconditionFailedError(nil).WithMessage("unable to update CLI secret under authmode: %s", a)
|
||||
}
|
||||
sctx, ok := security.FromContext(ctx)
|
||||
if !ok || !sctx.IsAuthenticated() {
|
||||
return errors.UnauthorizedError(nil)
|
||||
}
|
||||
if !matchUserID(sctx, id) && !sctx.Can(ctx, rbac.ActionUpdate, userResource) {
|
||||
return errors.ForbiddenError(nil).WithMessage("Not authorized to update the CLI secret for user: %d", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *usersAPI) requireCreatable(ctx context.Context) error {
|
||||
a, err := u.getAuth(ctx)
|
||||
if err != nil {
|
||||
log.G(ctx).Errorf("Failed to get authmode, error: %v", err)
|
||||
return err
|
||||
}
|
||||
if a != common.DBAuth {
|
||||
return errors.ForbiddenError(nil).WithMessage("creating local user is not allowed under auth mode: %s", a)
|
||||
}
|
||||
sr, err := config.SelfRegistration(ctx)
|
||||
if err != nil {
|
||||
log.G(ctx).Errorf("Failed to get self registration flag, error: %v", err)
|
||||
return err
|
||||
}
|
||||
accessErr := u.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceUser)
|
||||
if !sr {
|
||||
return accessErr
|
||||
}
|
||||
if accessErr != nil && !lib.GetCarrySession(ctx) {
|
||||
return errors.ForbiddenError(nil).WithMessage("self-registration cannot be triggered via API")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *usersAPI) requireReadable(ctx context.Context, id int) error {
|
||||
sctx, ok := security.FromContext(ctx)
|
||||
if !ok || !sctx.IsAuthenticated() {
|
||||
return errors.UnauthorizedError(nil)
|
||||
}
|
||||
if !matchUserID(sctx, id) && !sctx.Can(ctx, rbac.ActionRead, userResource) {
|
||||
return errors.ForbiddenError(nil).WithMessage("Not authorized to read user: %d", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *usersAPI) requireDeletable(ctx context.Context, id int) error {
|
||||
sctx, ok := security.FromContext(ctx)
|
||||
if !ok || !sctx.IsAuthenticated() {
|
||||
return errors.UnauthorizedError(nil)
|
||||
}
|
||||
a, err := u.getAuth(ctx)
|
||||
if err != nil {
|
||||
log.G(ctx).Errorf("Failed to get authmode, error: %v", err)
|
||||
return err
|
||||
}
|
||||
if a != common.DBAuth {
|
||||
return errors.ForbiddenError(nil).WithMessage("Deleting user is not allowed under auth mode: %s", a)
|
||||
}
|
||||
if !sctx.Can(ctx, rbac.ActionDelete, userResource) {
|
||||
return errors.ForbiddenError(nil).WithMessage("Not authorized to delete users")
|
||||
}
|
||||
if matchUserID(sctx, id) || id == 1 {
|
||||
return errors.ForbiddenError(nil).WithMessage("User with ID %d cannot be deleted", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *usersAPI) requireModifiable(ctx context.Context, id int) error {
|
||||
a, err := u.getAuth(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sctx, ok := security.FromContext(ctx)
|
||||
if !ok || !sctx.IsAuthenticated() {
|
||||
return errors.UnauthorizedError(nil)
|
||||
}
|
||||
if !modifiable(ctx, a, id) {
|
||||
return errors.ForbiddenError(nil).WithMessage("User with ID %d can't be updated", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func modifiable(ctx context.Context, authMode string, id int) bool {
|
||||
sctx, _ := security.FromContext(ctx)
|
||||
if authMode == common.DBAuth {
|
||||
|
||||
// In db auth, admin can update anyone's info, and regular user can update his own
|
||||
return sctx.Can(ctx, rbac.ActionUpdate, userResource) || matchUserID(sctx, id)
|
||||
}
|
||||
// In none db auth, only the local admin's password can be updated.
|
||||
return id == 1 && sctx.Can(ctx, rbac.ActionUpdate, userResource)
|
||||
}
|
||||
|
||||
func matchUserID(sctx security.Context, id int) bool {
|
||||
if localSCtx, ok := sctx.(*local.SecurityContext); ok {
|
||||
return localSCtx.User().UserID == id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func requireValidSecret(in string) error {
|
||||
hasLower := regexp.MustCompile(`[a-z]`)
|
||||
hasUpper := regexp.MustCompile(`[A-Z]`)
|
||||
hasNumber := regexp.MustCompile(`[0-9]`)
|
||||
if len(in) >= 8 && hasLower.MatchString(in) && hasUpper.MatchString(in) && hasNumber.MatchString(in) {
|
||||
return nil
|
||||
}
|
||||
return errors.BadRequestError(nil).WithMessage("the password or secret must be longer than 8 chars with at least 1 uppercase letter, 1 lowercase letter and 1 number")
|
||||
}
|
||||
|
||||
func validateUserProfile(user *usermodels.User) error {
|
||||
if len(user.Email) > 0 {
|
||||
if m, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, user.Email); !m {
|
||||
return errors.BadRequestError(nil).WithMessage("email with illegal format")
|
||||
}
|
||||
} else {
|
||||
return errors.BadRequestError(nil).WithMessage("email can't be empty")
|
||||
}
|
||||
|
||||
if utils.IsIllegalLength(user.Realname, 1, 255) {
|
||||
return errors.BadRequestError(nil).WithMessage("realname with illegal length")
|
||||
}
|
||||
|
||||
if utils.IsContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) {
|
||||
return errors.BadRequestError(nil).WithMessage("realname contains illegal characters")
|
||||
}
|
||||
if utils.IsIllegalLength(user.Comment, -1, 30) {
|
||||
return errors.BadRequestError(nil).WithMessage("comment with illegal length")
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
94
src/server/v2.0/handler/user_test.go
Normal file
94
src/server/v2.0/handler/user_test.go
Normal file
@ -0,0 +1,94 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
||||
usertesting "github.com/goharbor/harbor/src/testing/controller/user"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
htesting "github.com/goharbor/harbor/src/testing/server/v2.0/handler"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestRequireValidSecret(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
hasError bool
|
||||
}{
|
||||
{"", true},
|
||||
{"12345678", true},
|
||||
{"passw0rd", true},
|
||||
{"PASSW0RD", true},
|
||||
{"Sh0rt", true},
|
||||
{"Passw0rd", false},
|
||||
{"Thisis1Valid_password", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
e := requireValidSecret(c.in)
|
||||
assert.Equal(t, c.hasError, e != nil)
|
||||
}
|
||||
}
|
||||
|
||||
type UserTestSuite struct {
|
||||
htesting.Suite
|
||||
uCtl *usertesting.Controller
|
||||
}
|
||||
|
||||
func (uts *UserTestSuite) SetupSuite() {
|
||||
uts.uCtl = &usertesting.Controller{}
|
||||
uts.Config = &restapi.Config{
|
||||
UsersAPI: &usersAPI{
|
||||
ctl: uts.uCtl,
|
||||
getAuth: func(ctx context.Context) (string, error) {
|
||||
return common.DBAuth, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
uts.Suite.SetupSuite()
|
||||
uts.Security.On("IsAuthenticated").Return(true)
|
||||
|
||||
}
|
||||
|
||||
func (uts *UserTestSuite) TestUpdateUserPassword() {
|
||||
|
||||
body := models.PasswordReq{
|
||||
OldPassword: "Harbor12345",
|
||||
NewPassword: "Passw0rd",
|
||||
}
|
||||
{
|
||||
url := "/users/2/password"
|
||||
uts.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(false).Times(1)
|
||||
res, err := uts.Suite.PutJSON(url, &body)
|
||||
uts.NoError(err)
|
||||
uts.Equal(403, res.StatusCode)
|
||||
}
|
||||
{
|
||||
url := "/users/1/password"
|
||||
uts.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(1)
|
||||
uts.Security.On("GetUsername").Return("admin").Times(1)
|
||||
|
||||
uts.uCtl.On("VerifyPassword", mock.Anything, "admin", "Passw0rd").Return(true, nil).Times(1)
|
||||
res, err := uts.Suite.PutJSON(url, &body)
|
||||
uts.NoError(err)
|
||||
uts.Equal(400, res.StatusCode)
|
||||
}
|
||||
{
|
||||
url := "/users/1/password"
|
||||
uts.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(1)
|
||||
uts.Security.On("GetUsername").Return("admin").Times(1)
|
||||
|
||||
uts.uCtl.On("VerifyPassword", mock.Anything, "admin", mock.Anything).Return(false, nil).Times(1)
|
||||
uts.uCtl.On("UpdatePassword", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
res, err := uts.Suite.PutJSON(url, &body)
|
||||
uts.NoError(err)
|
||||
uts.Equal(200, res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserTestSuite(t *testing.T) {
|
||||
suite.Run(t, &UserTestSuite{})
|
||||
}
|
@ -24,13 +24,6 @@ import (
|
||||
func registerLegacyRoutes() {
|
||||
version := APIVersion
|
||||
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{})
|
||||
beego.Router("/api/"+version+"/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put")
|
||||
beego.Router("/api/"+version+"/users", &api.UserAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/"+version+"/users/search", &api.UserAPI{}, "get:Search")
|
||||
beego.Router("/api/"+version+"/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
|
||||
beego.Router("/api/"+version+"/users/:id/permissions", &api.UserAPI{}, "get:ListUserPermissions")
|
||||
beego.Router("/api/"+version+"/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
|
||||
beego.Router("/api/"+version+"/users/:id/cli_secret", &api.UserAPI{}, "put:SetCLISecret")
|
||||
beego.Router("/api/"+version+"/email/ping", &api.EmailAPI{}, "post:Ping")
|
||||
beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth")
|
||||
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
|
||||
|
165
src/testing/controller/config/controller.go
Normal file
165
src/testing/controller/config/controller.go
Normal file
@ -0,0 +1,165 @@
|
||||
// Code generated by mockery v2.1.0. DO NOT EDIT.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
libconfig "github.com/goharbor/harbor/src/lib/config"
|
||||
metadata "github.com/goharbor/harbor/src/lib/config/metadata"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Controller is an autogenerated mock type for the Controller type
|
||||
type Controller struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// AllConfigs provides a mock function with given fields: ctx
|
||||
func (_m *Controller) AllConfigs(ctx context.Context) (map[string]interface{}, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 map[string]interface{}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) map[string]interface{}); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, item
|
||||
func (_m *Controller) Get(ctx context.Context, item string) *metadata.ConfigureValue {
|
||||
ret := _m.Called(ctx, item)
|
||||
|
||||
var r0 *metadata.ConfigureValue
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *metadata.ConfigureValue); ok {
|
||||
r0 = rf(ctx, item)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*metadata.ConfigureValue)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetBool provides a mock function with given fields: ctx, item
|
||||
func (_m *Controller) GetBool(ctx context.Context, item string) bool {
|
||||
ret := _m.Called(ctx, item)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok {
|
||||
r0 = rf(ctx, item)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetInt provides a mock function with given fields: ctx, item
|
||||
func (_m *Controller) GetInt(ctx context.Context, item string) int {
|
||||
ret := _m.Called(ctx, item)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) int); ok {
|
||||
r0 = rf(ctx, item)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetManager provides a mock function with given fields:
|
||||
func (_m *Controller) GetManager() libconfig.Manager {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 libconfig.Manager
|
||||
if rf, ok := ret.Get(0).(func() libconfig.Manager); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(libconfig.Manager)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetString provides a mock function with given fields: ctx, item
|
||||
func (_m *Controller) GetString(ctx context.Context, item string) string {
|
||||
ret := _m.Called(ctx, item)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) string); ok {
|
||||
r0 = rf(ctx, item)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Load provides a mock function with given fields: ctx
|
||||
func (_m *Controller) Load(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateUserConfigs provides a mock function with given fields: ctx, conf
|
||||
func (_m *Controller) UpdateUserConfigs(ctx context.Context, conf map[string]interface{}) error {
|
||||
ret := _m.Called(ctx, conf)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, map[string]interface{}) error); ok {
|
||||
r0 = rf(ctx, conf)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UserConfigs provides a mock function with given fields: ctx
|
||||
func (_m *Controller) UserConfigs(ctx context.Context) (map[string]*libconfig.Value, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 map[string]*libconfig.Value
|
||||
if rf, ok := ret.Get(0).(func(context.Context) map[string]*libconfig.Value); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]*libconfig.Value)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
@ -26,3 +26,4 @@ package controller
|
||||
//go:generate mockery --case snake --dir ../../controller/proxy --name RemoteInterface --output ./proxy --outpkg proxy
|
||||
//go:generate mockery --case snake --dir ../../controller/retention --name Controller --output ./retention --outpkg retention
|
||||
//go:generate mockery --case snake --dir ../../controller/config --name Controller --output ./config --outpkg config
|
||||
//go:generate mockery --case snake --dir ../../controller/user --name Controller --output ./user --outpkg user
|
||||
|
198
src/testing/controller/user/controller.go
Normal file
198
src/testing/controller/user/controller.go
Normal file
@ -0,0 +1,198 @@
|
||||
// Code generated by mockery v2.1.0. DO NOT EDIT.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
models "github.com/goharbor/harbor/src/common/models"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
q "github.com/goharbor/harbor/src/lib/q"
|
||||
|
||||
user "github.com/goharbor/harbor/src/controller/user"
|
||||
)
|
||||
|
||||
// Controller is an autogenerated mock type for the Controller type
|
||||
type Controller struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Count provides a mock function with given fields: ctx, query
|
||||
func (_m *Controller) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, u
|
||||
func (_m *Controller) Create(ctx context.Context, u *models.User) (int, error) {
|
||||
ret := _m.Called(ctx, u)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.User) int); ok {
|
||||
r0 = rf(ctx, u)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *models.User) error); ok {
|
||||
r1 = rf(ctx, u)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, id
|
||||
func (_m *Controller) Delete(ctx context.Context, id int) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, id, opt
|
||||
func (_m *Controller) Get(ctx context.Context, id int, opt *user.Option) (*models.User, error) {
|
||||
ret := _m.Called(ctx, id, opt)
|
||||
|
||||
var r0 *models.User
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int, *user.Option) *models.User); ok {
|
||||
r0 = rf(ctx, id, opt)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.User)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int, *user.Option) error); ok {
|
||||
r1 = rf(ctx, id, opt)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, query
|
||||
func (_m *Controller) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*models.User
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*models.User); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*models.User)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetCliSecret provides a mock function with given fields: ctx, id, secret
|
||||
func (_m *Controller) SetCliSecret(ctx context.Context, id int, secret string) error {
|
||||
ret := _m.Called(ctx, id, secret)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int, string) error); ok {
|
||||
r0 = rf(ctx, id, secret)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetSysAdmin provides a mock function with given fields: ctx, id, adminFlag
|
||||
func (_m *Controller) SetSysAdmin(ctx context.Context, id int, adminFlag bool) error {
|
||||
ret := _m.Called(ctx, id, adminFlag)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int, bool) error); ok {
|
||||
r0 = rf(ctx, id, adminFlag)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdatePassword provides a mock function with given fields: ctx, id, password
|
||||
func (_m *Controller) UpdatePassword(ctx context.Context, id int, password string) error {
|
||||
ret := _m.Called(ctx, id, password)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int, string) error); ok {
|
||||
r0 = rf(ctx, id, password)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateProfile provides a mock function with given fields: ctx, u
|
||||
func (_m *Controller) UpdateProfile(ctx context.Context, u *models.User) error {
|
||||
ret := _m.Called(ctx, u)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.User) error); ok {
|
||||
r0 = rf(ctx, u)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// VerifyPassword provides a mock function with given fields: ctx, username, password
|
||||
func (_m *Controller) VerifyPassword(ctx context.Context, username string, password string) (bool, error) {
|
||||
ret := _m.Called(ctx, username, password)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok {
|
||||
r0 = rf(ctx, username, password)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, username, password)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
106
src/testing/pkg/oidc/dao/meta_dao.go
Normal file
106
src/testing/pkg/oidc/dao/meta_dao.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Code generated by mockery v2.1.0. DO NOT EDIT.
|
||||
|
||||
package oidc
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
models "github.com/goharbor/harbor/src/common/models"
|
||||
|
||||
q "github.com/goharbor/harbor/src/lib/q"
|
||||
)
|
||||
|
||||
// MetaDAO is an autogenerated mock type for the MetaDAO type
|
||||
type MetaDAO struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, oidcUser
|
||||
func (_m *MetaDAO) Create(ctx context.Context, oidcUser *models.OIDCUser) (int, error) {
|
||||
ret := _m.Called(ctx, oidcUser)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.OIDCUser) int); ok {
|
||||
r0 = rf(ctx, oidcUser)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *models.OIDCUser) error); ok {
|
||||
r1 = rf(ctx, oidcUser)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetByUsername provides a mock function with given fields: ctx, username
|
||||
func (_m *MetaDAO) GetByUsername(ctx context.Context, username string) (*models.OIDCUser, error) {
|
||||
ret := _m.Called(ctx, username)
|
||||
|
||||
var r0 *models.OIDCUser
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *models.OIDCUser); ok {
|
||||
r0 = rf(ctx, username)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.OIDCUser)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, username)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, query
|
||||
func (_m *MetaDAO) List(ctx context.Context, query *q.Query) ([]*models.OIDCUser, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*models.OIDCUser
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*models.OIDCUser); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*models.OIDCUser)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, oidcUser, props
|
||||
func (_m *MetaDAO) Update(ctx context.Context, oidcUser *models.OIDCUser, props ...string) error {
|
||||
_va := make([]interface{}, len(props))
|
||||
for _i := range props {
|
||||
_va[_i] = props[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, oidcUser)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.OIDCUser, ...string) error); ok {
|
||||
r0 = rf(ctx, oidcUser, props...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
@ -28,6 +28,8 @@ package pkg
|
||||
//go:generate mockery --case snake --dir ../../pkg/task --name Manager --output ./task --outpkg task
|
||||
//go:generate mockery --case snake --dir ../../pkg/task --name ExecutionManager --output ./task --outpkg task
|
||||
//go:generate mockery --case snake --dir ../../pkg/user --name Manager --output ./user --outpkg user
|
||||
//go:generate mockery --case snake --dir ../../pkg/user/dao --name DAO --output ./user/dao --outpkg dao
|
||||
//go:generate mockery --case snake --dir ../../pkg/oidc/dao --name MetaDAO --output ./oidc/dao --outpkg oidc
|
||||
//go:generate mockery --case snake --dir ../../pkg/rbac --name Manager --output ./rbac --outpkg rbac
|
||||
//go:generate mockery --case snake --dir ../../pkg/rbac/dao --name DAO --output ./rbac/dao --outpkg dao
|
||||
//go:generate mockery --case snake --dir ../../pkg/robot --name Manager --output ./robot --outpkg robot
|
||||
|
104
src/testing/pkg/user/dao/dao.go
Normal file
104
src/testing/pkg/user/dao/dao.go
Normal file
@ -0,0 +1,104 @@
|
||||
// Code generated by mockery v2.1.0. DO NOT EDIT.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
models "github.com/goharbor/harbor/src/common/models"
|
||||
|
||||
q "github.com/goharbor/harbor/src/lib/q"
|
||||
)
|
||||
|
||||
// DAO is an autogenerated mock type for the DAO type
|
||||
type DAO struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Count provides a mock function with given fields: ctx, query
|
||||
func (_m *DAO) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, user
|
||||
func (_m *DAO) Create(ctx context.Context, user *models.User) (int, error) {
|
||||
ret := _m.Called(ctx, user)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.User) int); ok {
|
||||
r0 = rf(ctx, user)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *models.User) error); ok {
|
||||
r1 = rf(ctx, user)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, query
|
||||
func (_m *DAO) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*models.User
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*models.User); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*models.User)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, user, props
|
||||
func (_m *DAO) Update(ctx context.Context, user *models.User, props ...string) error {
|
||||
_va := make([]interface{}, len(props))
|
||||
for _i := range props {
|
||||
_va[_i] = props[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, user)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.User, ...string) error); ok {
|
||||
r0 = rf(ctx, user, props...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
@ -18,6 +18,62 @@ type Manager struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Count provides a mock function with given fields: ctx, query
|
||||
func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, _a1
|
||||
func (_m *Manager) Create(ctx context.Context, _a1 *models.User) (int, error) {
|
||||
ret := _m.Called(ctx, _a1)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.User) int); ok {
|
||||
r0 = rf(ctx, _a1)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *models.User) error); ok {
|
||||
r1 = rf(ctx, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, id
|
||||
func (_m *Manager) Delete(ctx context.Context, id int) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, id
|
||||
func (_m *Manager) Get(ctx context.Context, id int) (*models.User, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
@ -86,3 +142,66 @@ func (_m *Manager) List(ctx context.Context, query *q.Query) (usermodels.Users,
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetSysAdminFlag provides a mock function with given fields: ctx, id, admin
|
||||
func (_m *Manager) SetSysAdminFlag(ctx context.Context, id int, admin bool) error {
|
||||
ret := _m.Called(ctx, id, admin)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int, bool) error); ok {
|
||||
r0 = rf(ctx, id, admin)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdatePassword provides a mock function with given fields: ctx, id, newPassword
|
||||
func (_m *Manager) UpdatePassword(ctx context.Context, id int, newPassword string) error {
|
||||
ret := _m.Called(ctx, id, newPassword)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int, string) error); ok {
|
||||
r0 = rf(ctx, id, newPassword)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateProfile provides a mock function with given fields: ctx, _a1
|
||||
func (_m *Manager) UpdateProfile(ctx context.Context, _a1 *models.User) error {
|
||||
ret := _m.Called(ctx, _a1)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.User) error); ok {
|
||||
r0 = rf(ctx, _a1)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// VerifyLocalPassword provides a mock function with given fields: ctx, username, password
|
||||
func (_m *Manager) VerifyLocalPassword(ctx context.Context, username string, password string) (bool, error) {
|
||||
ret := _m.Called(ctx, username, password)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok {
|
||||
r0 = rf(ctx, username, password)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, username, password)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
@ -29,7 +29,9 @@ def get_endpoint():
|
||||
|
||||
def _create_client(server, credential, debug, api_type="products"):
|
||||
cfg = None
|
||||
if api_type in ('projectv2', 'artifact', 'repository', 'scanner', 'scan', 'scanall', 'preheat', 'quota', 'replication', 'registry', 'robot', 'gc', 'retention', "immutable", "system_cve_allowlist", "configure"):
|
||||
if api_type in ('projectv2', 'artifact', 'repository', 'scanner', 'scan', 'scanall', 'preheat', 'quota',
|
||||
'replication', 'registry', 'robot', 'gc', 'retention', "immutable", "system_cve_allowlist",
|
||||
"configure", "users"):
|
||||
cfg = v2_swagger_client.Configuration()
|
||||
else:
|
||||
cfg = swagger_client.Configuration()
|
||||
@ -70,6 +72,7 @@ def _create_client(server, credential, debug, api_type="products"):
|
||||
"immutable": v2_swagger_client.ImmutableApi(v2_swagger_client.ApiClient(cfg)),
|
||||
"system_cve_allowlist": v2_swagger_client.SystemCVEAllowlistApi(v2_swagger_client.ApiClient(cfg)),
|
||||
"configure": v2_swagger_client.ConfigureApi(v2_swagger_client.ApiClient(cfg)),
|
||||
"users": v2_swagger_client.UsersApi(v2_swagger_client.ApiClient(cfg)),
|
||||
}.get(api_type,'Error: Wrong API type')
|
||||
|
||||
def _assert_status_code(expect_code, return_code, err_msg = r"HTTPS status code s not as we expected. Expected {}, while actual HTTPS status code is {}."):
|
||||
@ -121,8 +124,6 @@ def restart_process(process):
|
||||
raise Exception("Failed to start process {}.".format(full_process_name))
|
||||
run_command_with_popen("ps aux |grep " + full_process_name)
|
||||
|
||||
|
||||
|
||||
def run_command_with_popen(command):
|
||||
print("Command: ", command)
|
||||
|
||||
|
@ -1,28 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import base
|
||||
import swagger_client
|
||||
from swagger_client.rest import ApiException
|
||||
import v2_swagger_client
|
||||
from v2_swagger_client.rest import ApiException
|
||||
|
||||
class User(base.Base):
|
||||
|
||||
class User(base.Base, object):
|
||||
|
||||
def __init__(self):
|
||||
super(User, self).__init__(api_type = "users")
|
||||
|
||||
def create_user(self, name=None,
|
||||
email = None, user_password=None, realname = None, role_id = None, expect_status_code=201, **kwargs):
|
||||
email=None, user_password=None, realname=None, expect_status_code=201, **kwargs):
|
||||
if name is None:
|
||||
name = base._random_name("user")
|
||||
if realname is None:
|
||||
realname = base._random_name("realname")
|
||||
if email is None:
|
||||
email = '%s@%s.com' % (realname,"vmware")
|
||||
email = '%s@%s.com' % (realname, "vmware")
|
||||
if user_password is None:
|
||||
user_password = "Harbor12345678"
|
||||
if role_id is None:
|
||||
role_id = 0
|
||||
|
||||
user = swagger_client.User(username = name, email = email, password = user_password, realname = realname, role_id = role_id)
|
||||
|
||||
user_req = v2_swagger_client.UserCreationReq(username=name, email=email, password=user_password, realname=realname)
|
||||
try:
|
||||
_, status_code, header = self._get_client(**kwargs).users_post_with_http_info(user)
|
||||
_, status_code, header = self._get_client(**kwargs).create_user_with_http_info(user_req)
|
||||
except ApiException as e:
|
||||
base._assert_status_code(expect_status_code, e.status)
|
||||
else:
|
||||
@ -30,17 +30,21 @@ class User(base.Base):
|
||||
return base._get_id_from_header(header), name
|
||||
|
||||
def get_users(self, user_name=None, email=None, page=None, page_size=None, expect_status_code=200, **kwargs):
|
||||
params={}
|
||||
query = []
|
||||
if user_name is not None:
|
||||
params["username"] = user_name
|
||||
query.append("username=" + user_name)
|
||||
if email is not None:
|
||||
params["email"] = email
|
||||
query.append("email=" + email)
|
||||
|
||||
params = {}
|
||||
if len(query) > 0:
|
||||
params["q"] = ",".join(query)
|
||||
if page is not None:
|
||||
params["page"] = page
|
||||
if page_size is not None:
|
||||
params["page_size"] = page_size
|
||||
try:
|
||||
data, status_code, _ = self._get_client(**kwargs).users_get_with_http_info(**params)
|
||||
data, status_code, _ = self._get_client(**kwargs).list_users_with_http_info(**params)
|
||||
except ApiException as e:
|
||||
base._assert_status_code(expect_status_code, e.status)
|
||||
else:
|
||||
@ -48,44 +52,43 @@ class User(base.Base):
|
||||
return data
|
||||
|
||||
def get_user_by_id(self, user_id, **kwargs):
|
||||
data, status_code, _ = self._get_client(**kwargs).users_user_id_get_with_http_info(user_id)
|
||||
data, status_code, _ = self._get_client(**kwargs).get_user_with_http_info(user_id)
|
||||
base._assert_status_code(200, status_code)
|
||||
return data
|
||||
|
||||
def get_user_by_name(self, name, expect_status_code=200, **kwargs):
|
||||
users = self.get_users(user_name=name, expect_status_code=expect_status_code , **kwargs)
|
||||
users = self.get_users(user_name=name, expect_status_code=expect_status_code, **kwargs)
|
||||
for user in users:
|
||||
if user.username == name:
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
def get_user_current(self, **kwargs):
|
||||
data, status_code, _ = self._get_client(**kwargs).users_current_get_with_http_info()
|
||||
data, status_code, _ = self._get_client(**kwargs).get_current_user_info_with_http_info()
|
||||
base._assert_status_code(200, status_code)
|
||||
return data
|
||||
|
||||
def delete_user(self, user_id, expect_status_code = 200, **kwargs):
|
||||
_, status_code, _ = self._get_client(**kwargs).users_user_id_delete_with_http_info(user_id)
|
||||
def delete_user(self, user_id, expect_status_code=200, **kwargs):
|
||||
_, status_code, _ = self._get_client(**kwargs).delete_user_with_http_info(user_id)
|
||||
base._assert_status_code(expect_status_code, status_code)
|
||||
return user_id
|
||||
|
||||
def update_user_pwd(self, user_id, new_password=None, old_password=None, **kwargs):
|
||||
if old_password is None:
|
||||
old_password = ""
|
||||
password = swagger_client.Password(old_password, new_password)
|
||||
_, status_code, _ = self._get_client(**kwargs).users_user_id_password_put_with_http_info(user_id, password)
|
||||
old_password = ""
|
||||
password = v2_swagger_client.PasswordReq(old_password=old_password, new_password=new_password)
|
||||
_, status_code, _ = self._get_client(**kwargs).update_user_password_with_http_info(user_id, password)
|
||||
base._assert_status_code(200, status_code)
|
||||
return user_id
|
||||
|
||||
def update_user_profile(self, user_id, email=None, realname=None, comment=None, **kwargs):
|
||||
user_rofile = swagger_client.UserProfile(email, realname, comment)
|
||||
_, status_code, _ = self._get_client(**kwargs).users_user_id_put_with_http_info(user_id, user_rofile)
|
||||
user_profile = v2_swagger_client.UserProfile(email=email, realname=realname, comment=comment)
|
||||
_, status_code, _ = self._get_client(**kwargs).update_user_profile_with_http_info(user_id, user_profile)
|
||||
base._assert_status_code(200, status_code)
|
||||
return user_id
|
||||
|
||||
def update_user_role_as_sysadmin(self, user_id, IsAdmin, **kwargs):
|
||||
sysadmin_flag = swagger_client.SysAdminFlag(IsAdmin)
|
||||
_, status_code, _ = self._get_client(**kwargs).users_user_id_sysadmin_put_with_http_info(user_id, sysadmin_flag)
|
||||
sysadmin_flag = v2_swagger_client.UserSysAdminFlag(sysadmin_flag=IsAdmin)
|
||||
_, status_code, _ = self._get_client(**kwargs).set_user_sys_admin_with_http_info(user_id, sysadmin_flag)
|
||||
base._assert_status_code(200, status_code)
|
||||
return user_id
|
||||
|
Loading…
Reference in New Issue
Block a user