API for users to new model

This commit moves the legacy apis related to users to new model.
Some funcs under common/dao are left b/c they are used by other module,
which should also be shifted to leverage managers.
We'll handle them separately.

Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
Daniel Jiang 2021-04-09 19:40:29 +08:00
parent 3646b263da
commit d4cd2b87bd
35 changed files with 2610 additions and 1864 deletions

View File

@ -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:

View File

@ -4286,7 +4286,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
@ -4433,7 +4752,6 @@ parameters:
required: true
type: integer
format: int64
responses:
'200':
description: Success
@ -7043,3 +7361,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

View File

@ -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

View File

@ -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
}

View 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)
}

View File

@ -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")

View File

@ -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
}

View File

@ -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"))
}

View File

@ -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
View 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
}

View 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{})
}

View 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()}
}

View 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{})
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)

View File

@ -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{})
}

View File

@ -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
}

View 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)
}

View File

@ -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

View File

@ -58,6 +58,7 @@ func New() http.Handler {
SystemCVEAllowlistAPI: newSystemCVEAllowListAPI(),
ConfigureAPI: newConfigAPI(),
UsergroupAPI: newUserGroupAPI(),
UsersAPI: newUsersAPI(),
})
if err != nil {
log.Fatal(err)

View 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
}

View 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
}

View 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{})
}

View File

@ -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")

View 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
}

View File

@ -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

View 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
}

View 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
}

View File

@ -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

View 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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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