Feat: add user management apis (#3458)

* Feat: add user management apis

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Feat: add e2e test and some nit fix

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Feat: add password validate

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Feat: add email modification in update user

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Fix: fix user detail to user base

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Fix: fix ut

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Fix: fix test

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Fix: fix rebase

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Fix: add password check in create user

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Fix: fix bcode confilt

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
This commit is contained in:
Tianxin Dong 2022-03-19 15:51:32 +08:00 committed by GitHub
parent 13c420dada
commit 3ea2ac6d0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1803 additions and 215 deletions

View File

@ -132,5 +132,4 @@ def-install:
helm-doc-gen: helmdoc
readme-generator -v charts/vela-core/values.yaml -r charts/vela-core/README.md
cat charts/vela-core/README.md
readme-generator -v charts/vela-minimal/values.yaml -r charts/vela-minimal/README.md

View File

@ -2600,6 +2600,64 @@
}
}
},
"/api/v1/auth/dexConfig": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"authentication"
],
"summary": "get Dex config",
"operationId": "getDexConfig",
"responses": {
"200": {
"schema": {
"$ref": "#/definitions/v1.DexConfigResponse"
}
},
"400": {
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/auth/login": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"authentication"
],
"summary": "handle login request",
"operationId": "login",
"responses": {
"200": {
"schema": {
"$ref": "#/definitions/v1.LoginResponse"
}
},
"400": {
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/clusters": {
"get": {
"consumes": [
@ -3175,9 +3233,9 @@
"parameters": [
{
"enum": [
"workflowstep",
"component",
"trait",
"workflowstep"
"trait"
],
"type": "string",
"description": "query the definition type",
@ -4005,6 +4063,259 @@
}
}
},
"/api/v1/users": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"users"
],
"summary": "list users",
"operationId": "listUser",
"parameters": [
{
"type": "integer",
"description": "query the page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "query the page size number",
"name": "pageSize",
"in": "query"
},
{
"type": "string",
"description": "fuzzy search based on name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "fuzzy search based on email",
"name": "email",
"in": "query"
},
{
"type": "string",
"description": "fuzzy search based on alias",
"name": "alias",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ListUserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
},
"post": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"users"
],
"summary": "create a user",
"operationId": "createUser",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.CreateUserRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.DetailUserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/users/{username}": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"users"
],
"summary": "get user detail",
"operationId": "detailUser",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.DetailUserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
},
"put": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"users"
],
"summary": "update a user's alias or password",
"operationId": "updateUser",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.DetailUserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
},
"delete": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"users"
],
"summary": "delete a user",
"operationId": "deleteUser",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.EmptyResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/users/{username}/disable": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"users"
],
"summary": "disable a user",
"operationId": "disableUser",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.EmptyResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/users/{username}/enable": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"users"
],
"summary": "enable a user",
"operationId": "enableUser",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.EmptyResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/webhook/{token}": {
"post": {
"consumes": [
@ -5103,6 +5414,9 @@
}
}
},
"map[string]interface {}": {
"type": "object"
},
"model.ApplicationComponent": {
"required": [
"createTime",
@ -5304,8 +5618,8 @@
},
"model.Cluster": {
"required": [
"updateTime",
"createTime",
"updateTime",
"name",
"alias",
"description",
@ -5493,7 +5807,8 @@
"createTime",
"updateTime",
"installID",
"enableCollection"
"enableCollection",
"loginType"
],
"properties": {
"createTime": {
@ -5506,6 +5821,9 @@
"installID": {
"type": "string"
},
"loginType": {
"type": "string"
},
"updateTime": {
"type": "string",
"format": "date-time"
@ -6011,9 +6329,9 @@
},
"v1.ApplicationDeployResponse": {
"required": [
"createTime",
"version",
"envName",
"createTime",
"status",
"note",
"triggerType"
@ -6986,6 +7304,27 @@
}
}
},
"v1.CreateUserRequest": {
"required": [
"name",
"email",
"password"
],
"properties": {
"alias": {
"type": "string"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"v1.CreateWorkflowRequest": {
"required": [
"name",
@ -7051,11 +7390,11 @@
},
"v1.DetailAddonResponse": {
"required": [
"invisible",
"name",
"version",
"description",
"name",
"icon",
"invisible",
"version",
"schema",
"uiSchema",
"definitions"
@ -7128,13 +7467,13 @@
},
"v1.DetailApplicationResponse": {
"required": [
"createTime",
"updateTime",
"icon",
"name",
"alias",
"project",
"description",
"createTime",
"updateTime",
"policies",
"envBindings",
"status",
@ -7196,20 +7535,20 @@
},
"v1.DetailClusterResponse": {
"required": [
"name",
"status",
"reason",
"apiServerURL",
"dashboardURL",
"updateTime",
"name",
"icon",
"labels",
"createTime",
"provider",
"apiServerURL",
"alias",
"description",
"icon",
"status",
"labels",
"reason",
"dashboardURL",
"kubeConfig",
"kubeConfigSecret",
"alias",
"description",
"resourceInfo"
],
"properties": {
@ -7267,14 +7606,14 @@
},
"v1.DetailComponentResponse": {
"required": [
"name",
"appPrimaryKey",
"type",
"createTime",
"creator",
"alias",
"main",
"appPrimaryKey",
"creator",
"createTime",
"updateTime",
"main",
"name",
"type",
"definition"
],
"properties": {
@ -7376,13 +7715,13 @@
},
"v1.DetailPolicyResponse": {
"required": [
"createTime",
"updateTime",
"name",
"type",
"description",
"creator",
"properties",
"createTime"
"properties"
],
"properties": {
"createTime": {
@ -7412,17 +7751,17 @@
},
"v1.DetailRevisionResponse": {
"required": [
"createTime",
"updateTime",
"reason",
"deployUser",
"workflowName",
"version",
"triggerType",
"appPrimaryKey",
"status",
"note",
"envName"
"workflowName",
"appPrimaryKey",
"version",
"reason",
"triggerType",
"envName",
"createTime",
"status",
"updateTime",
"deployUser"
],
"properties": {
"appPrimaryKey": {
@ -7513,14 +7852,52 @@
}
}
},
"v1.DetailUserResponse": {
"required": [
"lastLoginTime",
"name",
"email",
"disabled",
"createTime",
"projects"
],
"properties": {
"alias": {
"type": "string"
},
"createTime": {
"type": "string",
"format": "date-time"
},
"disabled": {
"type": "boolean"
},
"email": {
"type": "string"
},
"lastLoginTime": {
"type": "string",
"format": "date-time"
},
"name": {
"type": "string"
},
"projects": {
"type": "array",
"items": {
"$ref": "#/definitions/v1.ProjectUserBase"
}
}
}
},
"v1.DetailWorkflowRecordResponse": {
"required": [
"name",
"namespace",
"workflowName",
"workflowAlias",
"applicationRevision",
"status",
"name",
"namespace",
"deployTime",
"deployUser",
"note",
@ -7572,12 +7949,12 @@
},
"v1.DetailWorkflowResponse": {
"required": [
"alias",
"description",
"default",
"envName",
"createTime",
"name",
"alias",
"description",
"enable",
"updateTime"
],
@ -7616,6 +7993,28 @@
}
}
},
"v1.DexConfigResponse": {
"required": [
"clientID",
"clientSecret",
"redirectURL",
"issuer"
],
"properties": {
"clientID": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"issuer": {
"type": "string"
},
"redirectURL": {
"type": "string"
}
}
},
"v1.EmptyResponse": {},
"v1.EnableAddonRequest": {
"properties": {
@ -7989,6 +8388,24 @@
}
}
},
"v1.ListUserResponse": {
"required": [
"users",
"total"
],
"properties": {
"total": {
"type": "integer",
"format": "int64"
},
"users": {
"type": "array",
"items": {
"$ref": "#/definitions/v1.DetailUserResponse"
}
}
}
},
"v1.ListWorkflowRecordsResponse": {
"required": [
"records",
@ -8020,6 +8437,22 @@
}
}
},
"v1.LoginResponse": {
"required": [
"userInfo"
],
"properties": {
"accessToken": {
"type": "string"
},
"refreshToken": {
"type": "string"
},
"userInfo": {
"$ref": "#/definitions/v1.DetailUserResponse"
}
}
},
"v1.NameAlias": {
"required": [
"name",
@ -8152,6 +8585,24 @@
}
}
},
"v1.ProjectUserBase": {
"required": [
"name",
"alias",
"userRole"
],
"properties": {
"alias": {
"type": "string"
},
"name": {
"type": "string"
},
"userRole": {
"type": "string"
}
}
},
"v1.PutApplicationEnvBindingRequest": {},
"v1.SimpleResponse": {
"required": [
@ -8175,10 +8626,11 @@
},
"v1.SystemInfoResponse": {
"required": [
"enableCollection",
"loginType",
"createTime",
"updateTime",
"installID",
"enableCollection",
"systemVersion"
],
"properties": {
@ -8192,6 +8644,9 @@
"installID": {
"type": "string"
},
"loginType": {
"type": "string"
},
"systemVersion": {
"$ref": "#/definitions/v1.SystemVersion"
},
@ -8386,6 +8841,37 @@
}
}
},
"v1.UserBase": {
"required": [
"createTime",
"lastLoginTime",
"name",
"email",
"disabled"
],
"properties": {
"alias": {
"type": "string"
},
"createTime": {
"type": "string",
"format": "date-time"
},
"disabled": {
"type": "boolean"
},
"email": {
"type": "string"
},
"lastLoginTime": {
"type": "string",
"format": "date-time"
},
"name": {
"type": "string"
}
}
},
"v1.VelaQLViewResponse": {
"type": "object"
},

2
go.mod
View File

@ -62,6 +62,7 @@ require (
github.com/wonderflow/cert-manager-api v1.0.3
go.mongodb.org/mongo-driver v1.5.1
go.uber.org/zap v1.18.1
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
golang.org/x/tools v0.1.6 // indirect
@ -246,7 +247,6 @@ require (
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect

View File

@ -16,20 +16,26 @@ limitations under the License.
package model
import "strings"
import (
"fmt"
"strings"
"time"
)
func init() {
RegisterModel(&User{})
RegisterModel(&ProjectUser{})
}
// User is the model of user
type User struct {
BaseModel
Name string `json:"name"`
Email string `json:"email"`
Alias string `json:"alias,omitempty"`
Password string `json:"password,omitempty"`
Disabled bool `json:"disabled"`
Name string `json:"name"`
Email string `json:"email"`
Alias string `json:"alias,omitempty"`
Password string `json:"password,omitempty"`
Disabled bool `json:"disabled"`
LastLoginTime time.Time `json:"lastLoginTime,omitempty"`
}
// TableName return custom table name
@ -59,6 +65,41 @@ func (u *User) Index() map[string]string {
return index
}
// ProjectUser is the model of user in project
type ProjectUser struct {
BaseModel
Username string `json:"username"`
ProjectName string `json:"projectName"`
UserRoles []string `json:"userRoles"`
}
// TableName return custom table name
func (u *ProjectUser) TableName() string {
return tableNamePrefix + "project_user"
}
// ShortTableName return custom table name
func (u *ProjectUser) ShortTableName() string {
return "pusr"
}
// PrimaryKey return custom primary key
func (u *ProjectUser) PrimaryKey() string {
return fmt.Sprintf("%s-%s", u.ProjectName, verifyUserValue(u.Username))
}
// Index return custom index
func (u *ProjectUser) Index() map[string]string {
index := make(map[string]string)
if u.Username != "" {
index["username"] = verifyUserValue(u.Username)
}
if u.ProjectName != "" {
index["projectName"] = u.ProjectName
}
return index
}
func verifyUserValue(v string) string {
s := strings.ReplaceAll(v, "@", "-")
s = strings.ReplaceAll(s, " ", "-")

View File

@ -43,6 +43,8 @@ var (
CtxKeyApplicationEnvBinding = "envbinding-policy"
// CtxKeyApplicationComponent request context key of component
CtxKeyApplicationComponent = "component"
// CtxKeyUser request context key of user
CtxKeyUser = "user"
)
// AddonPhase defines the phase of an addon
@ -1088,13 +1090,6 @@ type LoginResponse struct {
RefreshToken string `json:"refreshToken,omitempty"`
}
// DetailUserResponse is the detail user info for the response
type DetailUserResponse struct {
Name string `json:"name"`
Alias string `json:"alias,omitempty"`
Email string `json:"email"`
}
// DexConfigResponse is the response of dex config
type DexConfigResponse struct {
ClientID string `json:"clientID"`
@ -1102,3 +1097,54 @@ type DexConfigResponse struct {
RedirectURL string `json:"redirectURL"`
Issuer string `json:"issuer"`
}
// DetailUserResponse is the response of user detail
type DetailUserResponse struct {
UserBase
Projects []ProjectUserBase `json:"projects"`
}
// ProjectUserBase project user base
type ProjectUserBase struct {
Name string `json:"name"`
Alias string `json:"alias"`
UserRoles []string `json:"userRoles"`
}
// CreateUserRequest create user request
type CreateUserRequest struct {
Name string `json:"name" validate:"checkname"`
Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"`
Email string `json:"email" validate:"checkemail"`
Password string `json:"password" validate:"checkpassword"`
}
// UpdateUserRequest update user request
type UpdateUserRequest struct {
Alias string `json:"alias,omitempty" optional:"true"`
Password string `json:"password,omitempty" validate:"checkpassword" optional:"true"`
Email string `json:"email,omitempty" validate:"checkemail" optional:"true"`
}
// ListUserResponse list user response
type ListUserResponse struct {
Users []*DetailUserResponse `json:"users"`
Total int64 `json:"total"`
}
// UserBase is the base info of user
type UserBase struct {
CreateTime time.Time `json:"createTime"`
LastLoginTime time.Time `json:"lastLoginTime"`
Name string `json:"name"`
Email string `json:"email"`
Alias string `json:"alias,omitempty"`
Disabled bool `json:"disabled"`
}
// ListUserOptions list user options
type ListUserOptions struct {
Name string `json:"name"`
Email string `json:"email"`
Alias string `json:"alias"`
}

View File

@ -195,8 +195,10 @@ func (d *dexHandlerImpl) login(ctx context.Context) (*apisv1.LoginResponse, erro
return &apisv1.LoginResponse{
UserInfo: apisv1.DetailUserResponse{
Name: claims.Name,
Email: claims.Email,
UserBase: apisv1.UserBase{
Name: claims.Name,
Email: claims.Email,
},
},
AccessToken: d.token.AccessToken,
RefreshToken: d.token.RefreshToken,

View File

@ -67,14 +67,14 @@
- description: The value of the environment variable
jsonKey: value
label: Value
sort: 100
sort: 101
uiType: Input
validate:
immutable: false
- description: Specifies a source the value of this var should come from
jsonKey: valueFrom
label: Secret Selector
sort: 100
sort: 102
subParameters:
- description: Selects a key of a secret in the pod's namespace
jsonKey: secretKeyRef
@ -124,64 +124,11 @@
defaultValue: 3
immutable: false
required: true
- description: Instructions for assessing container health by executing an HTTP
GET request. Either this attribute or the exec attribute or the tcpSocket attribute
MUST be specified. This attribute is mutually exclusive with both the exec attribute
and the tcpSocket attribute.
jsonKey: httpGet
label: HttpGet
sort: 100
subParameters:
- description: The TCP socket within the container to which the HTTP GET request
should be directed.
jsonKey: port
label: Port
sort: 100
uiType: Number
validate:
immutable: false
required: true
- description: ""
jsonKey: httpHeaders
label: HttpHeaders
sort: 100
subParameters:
- description: ""
jsonKey: name
label: Name
sort: 100
uiType: Input
validate:
immutable: false
required: true
- description: ""
jsonKey: value
label: Value
sort: 100
uiType: Input
validate:
immutable: false
required: true
uiType: Structs
validate:
immutable: false
- description: The endpoint, relative to the port, to which the HTTP GET request
should be directed.
jsonKey: path
label: Path
sort: 100
uiType: Input
validate:
immutable: false
required: true
uiType: Group
validate:
immutable: false
- description: Number of seconds after the container is started before the first
probe is initiated.
jsonKey: initialDelaySeconds
label: InitialDelaySeconds
sort: 100
sort: 101
uiType: Number
validate:
defaultValue: 0
@ -190,7 +137,7 @@
- description: How often, in seconds, to execute the probe.
jsonKey: periodSeconds
label: PeriodSeconds
sort: 100
sort: 102
uiType: Number
validate:
defaultValue: 10
@ -200,19 +147,50 @@
after having failed.
jsonKey: successThreshold
label: SuccessThreshold
sort: 100
sort: 103
uiType: Number
validate:
defaultValue: 1
immutable: false
required: true
- description: Number of seconds after which the probe times out.
jsonKey: timeoutSeconds
label: TimeoutSeconds
sort: 104
uiType: Number
validate:
defaultValue: 1
immutable: false
required: true
- description: Instructions for assessing container health by executing a command.
Either this attribute or the httpGet attribute or the tcpSocket attribute MUST
be specified. This attribute is mutually exclusive with both the httpGet attribute
and the tcpSocket attribute.
jsonKey: exec
label: Exec
sort: 105
subParameters:
- description: A command to be executed inside the container to assess its health.
Each space delimited token of the command is a separate array element. Commands
exiting 0 are considered to be successful probes, whilst all other exit codes
are considered failures.
jsonKey: command
label: Command
sort: 100
uiType: Strings
validate:
immutable: false
required: true
uiType: Group
validate:
immutable: false
- description: Instructions for assessing container health by probing a TCP socket.
Either this attribute or the exec attribute or the httpGet attribute MUST be
specified. This attribute is mutually exclusive with both the exec attribute
and the httpGet attribute.
jsonKey: tcpSocket
label: TcpSocket
sort: 100
sort: 106
subParameters:
- description: The TCP socket within the container that should be probed to assess
container health.
@ -226,34 +204,56 @@
uiType: Group
validate:
immutable: false
- description: Number of seconds after which the probe times out.
jsonKey: timeoutSeconds
label: TimeoutSeconds
sort: 100
uiType: Number
validate:
defaultValue: 1
immutable: false
required: true
- description: Instructions for assessing container health by executing a command.
Either this attribute or the httpGet attribute or the tcpSocket attribute MUST
be specified. This attribute is mutually exclusive with both the httpGet attribute
- description: Instructions for assessing container health by executing an HTTP
GET request. Either this attribute or the exec attribute or the tcpSocket attribute
MUST be specified. This attribute is mutually exclusive with both the exec attribute
and the tcpSocket attribute.
jsonKey: exec
label: Exec
sort: 100
jsonKey: httpGet
label: HttpGet
sort: 107
subParameters:
- description: A command to be executed inside the container to assess its health.
Each space delimited token of the command is a separate array element. Commands
exiting 0 are considered to be successful probes, whilst all other exit codes
are considered failures.
jsonKey: command
label: Command
- description: The endpoint, relative to the port, to which the HTTP GET request
should be directed.
jsonKey: path
label: Path
sort: 100
uiType: Strings
uiType: Input
validate:
immutable: false
required: true
- description: The TCP socket within the container to which the HTTP GET request
should be directed.
jsonKey: port
label: Port
sort: 101
uiType: Number
validate:
immutable: false
required: true
- description: ""
jsonKey: httpHeaders
label: HttpHeaders
sort: 102
subParameters:
- description: ""
jsonKey: name
label: Name
sort: 100
uiType: Input
validate:
immutable: false
required: true
- description: ""
jsonKey: value
label: Value
sort: 101
uiType: Input
validate:
immutable: false
required: true
uiType: Structs
validate:
immutable: false
uiType: Group
validate:
immutable: false
@ -265,11 +265,21 @@
label: LivenessProbe
sort: 15
subParameters:
- description: Number of consecutive failures required to determine the container
is not alive (liveness probe) or not ready (readiness probe).
jsonKey: failureThreshold
label: FailureThreshold
sort: 100
uiType: Number
validate:
defaultValue: 3
immutable: false
required: true
- description: Number of seconds after the container is started before the first
probe is initiated.
jsonKey: initialDelaySeconds
label: InitialDelaySeconds
sort: 100
sort: 101
uiType: Number
validate:
defaultValue: 0
@ -278,7 +288,7 @@
- description: How often, in seconds, to execute the probe.
jsonKey: periodSeconds
label: PeriodSeconds
sort: 100
sort: 102
uiType: Number
validate:
defaultValue: 10
@ -288,36 +298,16 @@
after having failed.
jsonKey: successThreshold
label: SuccessThreshold
sort: 100
sort: 103
uiType: Number
validate:
defaultValue: 1
immutable: false
required: true
- description: Instructions for assessing container health by probing a TCP socket.
Either this attribute or the exec attribute or the httpGet attribute MUST be
specified. This attribute is mutually exclusive with both the exec attribute
and the httpGet attribute.
jsonKey: tcpSocket
label: TcpSocket
sort: 100
subParameters:
- description: The TCP socket within the container that should be probed to assess
container health.
jsonKey: port
label: Port
sort: 100
uiType: Number
validate:
immutable: false
required: true
uiType: Group
validate:
immutable: false
- description: Number of seconds after which the probe times out.
jsonKey: timeoutSeconds
label: TimeoutSeconds
sort: 100
sort: 104
uiType: Number
validate:
defaultValue: 1
@ -329,7 +319,7 @@
and the tcpSocket attribute.
jsonKey: exec
label: Exec
sort: 100
sort: 105
subParameters:
- description: A command to be executed inside the container to assess its health.
Each space delimited token of the command is a separate array element. Commands
@ -345,48 +335,34 @@
uiType: Group
validate:
immutable: false
- description: Number of consecutive failures required to determine the container
is not alive (liveness probe) or not ready (readiness probe).
jsonKey: failureThreshold
label: FailureThreshold
sort: 100
uiType: Number
- description: Instructions for assessing container health by probing a TCP socket.
Either this attribute or the exec attribute or the httpGet attribute MUST be
specified. This attribute is mutually exclusive with both the exec attribute
and the httpGet attribute.
jsonKey: tcpSocket
label: TcpSocket
sort: 106
subParameters:
- description: The TCP socket within the container that should be probed to assess
container health.
jsonKey: port
label: Port
sort: 100
uiType: Number
validate:
immutable: false
required: true
uiType: Group
validate:
defaultValue: 3
immutable: false
required: true
- description: Instructions for assessing container health by executing an HTTP
GET request. Either this attribute or the exec attribute or the tcpSocket attribute
MUST be specified. This attribute is mutually exclusive with both the exec attribute
and the tcpSocket attribute.
jsonKey: httpGet
label: HttpGet
sort: 100
sort: 107
subParameters:
- description: ""
jsonKey: httpHeaders
label: HttpHeaders
sort: 100
subParameters:
- description: ""
jsonKey: name
label: Name
sort: 100
uiType: Input
validate:
immutable: false
required: true
- description: ""
jsonKey: value
label: Value
sort: 100
uiType: Input
validate:
immutable: false
required: true
uiType: Structs
validate:
immutable: false
- description: The endpoint, relative to the port, to which the HTTP GET request
should be directed.
jsonKey: path
@ -400,27 +376,41 @@
should be directed.
jsonKey: port
label: Port
sort: 100
sort: 101
uiType: Number
validate:
immutable: false
required: true
- description: ""
jsonKey: httpHeaders
label: HttpHeaders
sort: 102
subParameters:
- description: ""
jsonKey: name
label: Name
sort: 100
uiType: Input
validate:
immutable: false
required: true
- description: ""
jsonKey: value
label: Value
sort: 101
uiType: Input
validate:
immutable: false
required: true
uiType: Structs
validate:
immutable: false
uiType: Group
validate:
immutable: false
uiType: Group
validate:
immutable: false
- description: Which port do you want customer traffic sent to
disable: true
jsonKey: port
label: Port
sort: 100
uiType: Number
validate:
defaultValue: 80
immutable: false
required: true
- description: If addRevisionLabel is true, the appRevision label will be added to
the underlying pods
disable: true
@ -432,10 +422,20 @@
defaultValue: false
immutable: false
required: true
- description: Which port do you want customer traffic sent to
disable: true
jsonKey: port
label: Port
sort: 102
uiType: Number
validate:
defaultValue: 80
immutable: false
required: true
- description: Specify image pull secrets for your service
jsonKey: imagePullSecrets
label: ImagePullSecrets
sort: 100
sort: 106
uiType: Strings
validate:
immutable: false
@ -443,7 +443,7 @@
disable: true
jsonKey: volumes
label: Volumes
sort: 100
sort: 109
subParameters:
- description: ""
jsonKey: mountPath
@ -456,7 +456,7 @@
- description: ""
jsonKey: name
label: Name
sort: 100
sort: 101
uiType: Input
validate:
immutable: false
@ -464,7 +464,7 @@
- description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"'
jsonKey: type
label: Type
sort: 100
sort: 102
uiType: Select
validate:
immutable: false

View File

@ -0,0 +1,279 @@
/*
Copyright 2022 The KubeVela 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 usecase
import (
"context"
"golang.org/x/crypto/bcrypt"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
)
// UserUsecase User manage api
type UserUsecase interface {
GetUser(ctx context.Context, username string) (*model.User, error)
DetailUser(ctx context.Context, user *model.User) (*apisv1.DetailUserResponse, error)
DeleteUser(ctx context.Context, username string) error
CreateUser(ctx context.Context, req apisv1.CreateUserRequest) (*apisv1.UserBase, error)
UpdateUser(ctx context.Context, user *model.User, req apisv1.UpdateUserRequest) (*apisv1.UserBase, error)
ListUsers(ctx context.Context, page, pageSize int, listOptions apisv1.ListUserOptions) (*apisv1.ListUserResponse, error)
DisableUser(ctx context.Context, user *model.User) error
EnableUser(ctx context.Context, user *model.User) error
}
type userUsecaseImpl struct {
ds datastore.DataStore
k8sClient client.Client
projectUsecase ProjectUsecase
sysUsecase SystemInfoUsecase
}
// NewUserUsecase new User usecase
func NewUserUsecase(ds datastore.DataStore, projectUsecase ProjectUsecase, sysUsecase SystemInfoUsecase) UserUsecase {
k8sClient, err := clients.GetKubeClient()
if err != nil {
log.Logger.Fatalf("get k8sClient failure: %s", err.Error())
}
return &userUsecaseImpl{
k8sClient: k8sClient,
ds: ds,
projectUsecase: projectUsecase,
sysUsecase: sysUsecase,
}
}
// GetUser get user
func (u *userUsecaseImpl) GetUser(ctx context.Context, username string) (*model.User, error) {
user := &model.User{
Name: username,
}
if err := u.ds.Get(ctx, user); err != nil {
return nil, err
}
return user, nil
}
// DetailUser return user detail
func (u *userUsecaseImpl) DetailUser(ctx context.Context, user *model.User) (*apisv1.DetailUserResponse, error) {
detailUser := convertUserModel(user)
pUser := &model.ProjectUser{
Username: user.Name,
}
projectUsers, err := u.ds.List(ctx, pUser, &datastore.ListOptions{
SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}},
})
if err != nil {
return nil, err
}
for _, v := range projectUsers {
pu, ok := v.(*model.ProjectUser)
if ok {
project, err := u.projectUsecase.GetProject(ctx, pu.ProjectName)
if err != nil {
log.Logger.Errorf("failed to delete project(%s) info: %s", pu.ProjectName, err.Error())
continue
}
detailUser.Projects = append(detailUser.Projects, apisv1.ProjectUserBase{
Name: pu.ProjectName,
Alias: project.Alias,
UserRoles: pu.UserRoles,
})
}
}
return detailUser, nil
}
// DeleteUser delete user
func (u *userUsecaseImpl) DeleteUser(ctx context.Context, username string) error {
pUser := &model.ProjectUser{
Username: username,
}
projectUsers, err := u.ds.List(ctx, pUser, &datastore.ListOptions{})
if err != nil {
return err
}
for _, v := range projectUsers {
pu := v.(*model.ProjectUser)
if err := u.ds.Delete(ctx, pu); err != nil {
log.Logger.Errorf("failed to delete project user %s: %s", pu.PrimaryKey(), err.Error())
}
}
if err := u.ds.Delete(ctx, &model.User{Name: username}); err != nil {
log.Logger.Errorf("failed to delete user", username, err.Error())
return err
}
return nil
}
// CreateUser create user
func (u *userUsecaseImpl) CreateUser(ctx context.Context, req apisv1.CreateUserRequest) (*apisv1.UserBase, error) {
sysInfo, err := u.sysUsecase.GetSystemInfo(ctx)
if err != nil {
return nil, err
}
if sysInfo.LoginType == model.LoginTypeDex {
return nil, bcode.ErrUserCannotModified
}
hash, err := generatePasswordHash(req.Password)
if err != nil {
return nil, err
}
user := &model.User{
Name: req.Name,
Alias: req.Alias,
Email: req.Email,
Password: hash,
Disabled: false,
}
if err := u.ds.Add(ctx, user); err != nil {
return nil, err
}
return convertUserBase(user), nil
}
// UpdateUser update user
func (u *userUsecaseImpl) UpdateUser(ctx context.Context, user *model.User, req apisv1.UpdateUserRequest) (*apisv1.UserBase, error) {
sysInfo, err := u.sysUsecase.GetSystemInfo(ctx)
if err != nil {
return nil, err
}
if sysInfo.LoginType == model.LoginTypeDex {
return nil, bcode.ErrUserCannotModified
}
if req.Alias != "" {
user.Alias = req.Alias
}
if req.Password != "" {
hash, err := generatePasswordHash(req.Password)
if err != nil {
return nil, err
}
user.Password = hash
}
if req.Email != "" {
if user.Email != "" {
return nil, bcode.ErrUnsupportedEmailModification
}
user.Email = req.Email
}
if err := u.ds.Put(ctx, user); err != nil {
return nil, err
}
return convertUserBase(user), nil
}
// ListUsers list users
func (u *userUsecaseImpl) ListUsers(ctx context.Context, page, pageSize int, listOptions apisv1.ListUserOptions) (*apisv1.ListUserResponse, error) {
user := &model.User{}
var queries []datastore.FuzzyQueryOption
if listOptions.Name != "" {
queries = append(queries, datastore.FuzzyQueryOption{Key: "name", Query: listOptions.Name})
}
if listOptions.Email != "" {
queries = append(queries, datastore.FuzzyQueryOption{Key: "email", Query: listOptions.Email})
}
if listOptions.Alias != "" {
queries = append(queries, datastore.FuzzyQueryOption{Key: "alias", Query: listOptions.Alias})
}
fo := datastore.FilterOptions{Queries: queries}
var userList []*apisv1.DetailUserResponse
users, err := u.ds.List(ctx, user, &datastore.ListOptions{
Page: page,
PageSize: pageSize,
SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}},
FilterOptions: fo,
})
if err != nil {
return nil, err
}
for _, v := range users {
user, ok := v.(*model.User)
if ok {
userList = append(userList, convertUserModel(user))
}
}
count, err := u.ds.Count(ctx, user, &fo)
if err != nil {
return nil, err
}
return &apisv1.ListUserResponse{
Users: userList,
Total: count,
}, nil
}
// DisableUser disable user
func (u *userUsecaseImpl) DisableUser(ctx context.Context, user *model.User) error {
if user.Disabled {
return bcode.ErrUserAlreadyDisabled
}
user.Disabled = true
return u.ds.Put(ctx, user)
}
// EnableUser disable user
func (u *userUsecaseImpl) EnableUser(ctx context.Context, user *model.User) error {
if !user.Disabled {
return bcode.ErrUserAlreadyEnabled
}
user.Disabled = false
return u.ds.Put(ctx, user)
}
func convertUserModel(user *model.User) *apisv1.DetailUserResponse {
return &apisv1.DetailUserResponse{
UserBase: *convertUserBase(user),
Projects: make([]apisv1.ProjectUserBase, 0),
}
}
func convertUserBase(user *model.User) *apisv1.UserBase {
return &apisv1.UserBase{
Name: user.Name,
Alias: user.Alias,
Email: user.Email,
CreateTime: user.CreateTime,
LastLoginTime: user.LastLoginTime,
Disabled: user.Disabled,
}
}
func generatePasswordHash(s string) (string, error) {
if s == "" {
return "", bcode.ErrUserInvalidPassword
}
hashed, err := bcrypt.GenerateFromPassword([]byte(s), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashed), nil
}
func compareHashWithPassword(hash, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}

View File

@ -0,0 +1,241 @@
/*
Copyright 2022 The KubeVela 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 usecase
import (
"context"
"fmt"
"strconv"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
)
var _ = Describe("Test authentication usecase functions", func() {
var (
userUsecase *userUsecaseImpl
ds datastore.DataStore
db string
)
BeforeEach(func() {
var err error
db = "user-test-" + strconv.FormatInt(time.Now().UnixNano(), 10)
ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: db})
Expect(ds).ToNot(BeNil())
Expect(err).Should(BeNil())
projectUsecase := &projectUsecaseImpl{k8sClient: k8sClient, ds: ds}
sysUsecase := &systemInfoUsecaseImpl{ds: ds}
userUsecase = &userUsecaseImpl{ds: ds, projectUsecase: projectUsecase, sysUsecase: sysUsecase}
})
AfterEach(func() {
err := k8sClient.Delete(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: db}})
Expect(err).Should(BeNil())
})
It("Test create user", func() {
user, err := userUsecase.CreateUser(context.Background(), apisv1.CreateUserRequest{
Name: "name",
Alias: "alias",
Email: "email@example.com",
Password: "password",
})
Expect(err).Should(BeNil())
Expect(user.Name).Should(Equal("name"))
Expect(user.Alias).Should(Equal("alias"))
Expect(user.Email).Should(Equal("email@example.com"))
u := &model.User{
Name: "name",
}
err = ds.Get(context.Background(), u)
Expect(err).Should(BeNil())
Expect(u.Name).Should(Equal("name"))
Expect(u.Alias).Should(Equal("alias"))
Expect(u.Email).Should(Equal("email@example.com"))
Expect(u.Disabled).Should(Equal(false))
Expect(compareHashWithPassword(u.Password, "password")).Should(BeNil())
})
It("Test detail user", func() {
ctx := context.Background()
err := ds.Add(ctx, &model.User{
Name: "name",
Alias: "alias",
Email: "email@example.com",
Password: "password",
})
Expect(err).Should(BeNil())
for i := 0; i < 2; i++ {
err = ds.Add(ctx, &model.ProjectUser{
Username: "name",
ProjectName: fmt.Sprintf("project-%d", i),
UserRoles: []string{fmt.Sprintf("user-role-%d", i)},
})
Expect(err).Should(BeNil())
err = ds.Add(ctx, &model.Project{
Name: fmt.Sprintf("project-%d", i),
Alias: fmt.Sprintf("project-alias-%d", i),
})
Expect(err).Should(BeNil())
}
userModel := &model.User{
Name: "name",
}
err = ds.Get(ctx, userModel)
Expect(err).Should(BeNil())
user, err := userUsecase.DetailUser(ctx, userModel)
Expect(err).Should(BeNil())
Expect(user.Name).Should(Equal("name"))
Expect(user.Alias).Should(Equal("alias"))
Expect(user.Email).Should(Equal("email@example.com"))
Expect(user.Projects).Should(Equal([]apisv1.ProjectUserBase{
{
Name: "project-1",
Alias: "project-alias-1",
UserRoles: []string{"user-role-1"},
},
{
Name: "project-0",
Alias: "project-alias-0",
UserRoles: []string{"user-role-0"},
},
}))
})
It("Test list users", func() {
ctx := context.Background()
for i := 0; i < 2; i++ {
err := ds.Add(ctx, &model.User{
Name: fmt.Sprintf("name-%d", i),
})
Expect(err).Should(BeNil())
}
users, err := userUsecase.ListUsers(ctx, 0, 10, apisv1.ListUserOptions{Name: "1"})
Expect(err).Should(BeNil())
Expect(users.Total).Should(Equal(int64(1)))
users, err = userUsecase.ListUsers(ctx, 0, 10, apisv1.ListUserOptions{})
Expect(err).Should(BeNil())
Expect(users.Total).Should(Equal(int64(2)))
})
It("Test delete user", func() {
ctx := context.Background()
err := ds.Add(ctx, &model.User{
Name: "name",
Alias: "alias",
Email: "email@example.com",
Password: "password",
})
Expect(err).Should(BeNil())
users, err := userUsecase.ListUsers(ctx, 0, 10, apisv1.ListUserOptions{})
Expect(err).Should(BeNil())
Expect(users.Total).Should(Equal(int64(1)))
err = userUsecase.DeleteUser(ctx, "name")
Expect(err).Should(BeNil())
users, err = userUsecase.ListUsers(ctx, 0, 10, apisv1.ListUserOptions{})
Expect(err).Should(BeNil())
Expect(users.Total).Should(Equal(int64(0)))
})
It("Test update user", func() {
ctx := context.Background()
userModel := &model.User{
Name: "name",
Alias: "alias",
Email: "email@example.com",
Password: "password",
}
err := ds.Add(ctx, userModel)
Expect(err).Should(BeNil())
_, err = userUsecase.UpdateUser(ctx, userModel, apisv1.UpdateUserRequest{
Alias: "new-alias",
Password: "new-password",
})
Expect(err).Should(BeNil())
newUser := &model.User{
Name: "name",
}
err = ds.Get(ctx, newUser)
Expect(err).Should(BeNil())
Expect(newUser.Alias).Should(Equal("new-alias"))
Expect(compareHashWithPassword(newUser.Password, "new-password")).Should(BeNil())
})
It("Test disable user", func() {
ctx := context.Background()
userModel := &model.User{
Name: "name",
Disabled: true,
}
err := ds.Add(ctx, userModel)
Expect(err).Should(BeNil())
err = userUsecase.DisableUser(ctx, userModel)
Expect(err).Should(Equal(bcode.ErrUserAlreadyDisabled))
userModel.Disabled = false
err = ds.Put(ctx, userModel)
Expect(err).Should(BeNil())
err = userUsecase.DisableUser(ctx, userModel)
Expect(err).Should(BeNil())
newUser := &model.User{
Name: "name",
}
err = ds.Get(ctx, newUser)
Expect(err).Should(BeNil())
Expect(newUser.Disabled).Should(Equal(true))
})
It("Test enable user", func() {
ctx := context.Background()
userModel := &model.User{
Name: "name",
Disabled: false,
}
err := ds.Add(ctx, userModel)
Expect(err).Should(BeNil())
err = userUsecase.EnableUser(ctx, userModel)
Expect(err).Should(Equal(bcode.ErrUserAlreadyEnabled))
userModel.Disabled = true
err = ds.Put(ctx, userModel)
Expect(err).Should(BeNil())
err = userUsecase.EnableUser(ctx, userModel)
Expect(err).Should(BeNil())
newUser := &model.User{
Name: "name",
}
err = ds.Get(ctx, newUser)
Expect(err).Should(BeNil())
Expect(newUser.Disabled).Should(Equal(false))
})
})

View File

@ -0,0 +1,30 @@
/*
Copyright 2022 The KubeVela 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 bcode
var (
// ErrUnsupportedEmailModification is the error of unsupported email modification
ErrUnsupportedEmailModification = NewBcode(400, 14001, "the user already has an email address and cannot modify it again")
// ErrUserAlreadyDisabled is the error of user already disabled
ErrUserAlreadyDisabled = NewBcode(400, 14002, "the user is already disabled")
// ErrUserAlreadyEnabled is the error of user already enabled
ErrUserAlreadyEnabled = NewBcode(400, 14003, "the user is already enabled")
// ErrUserCannotModified is the error of user cannot modified
ErrUserCannotModified = NewBcode(400, 14004, "the user cannot be modified in dex login mode")
// ErrUserInvalidPassword is the error of user invalid password
ErrUserInvalidPassword = NewBcode(400, 14005, "the password is invalid")
)

View File

@ -38,7 +38,7 @@ func NewAuthenticationWebService(authenticationUsecase usecase.AuthenticationUse
func (c *authenticationWebService) GetWebService() *restful.WebService {
ws := new(restful.WebService)
ws.Path(versionPrefix).
ws.Path(versionPrefix+"/auth").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML).
Doc("api for authentication manage")

View File

@ -0,0 +1,236 @@
/*
Copyright 2022 The KubeVela 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 webservice
import (
"context"
restfulspec "github.com/emicklei/go-restful-openapi/v2"
"github.com/emicklei/go-restful/v3"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
)
type userWebService struct {
userUsecase usecase.UserUsecase
}
// NewUserWebService is the webservice of user
func NewUserWebService(userUsecase usecase.UserUsecase) WebService {
return &userWebService{
userUsecase: userUsecase,
}
}
func (c *userWebService) GetWebService() *restful.WebService {
ws := new(restful.WebService)
ws.Path(versionPrefix+"/users").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML).
Doc("api for user manage")
tags := []string{"users"}
ws.Route(ws.GET("/").To(c.listUser).
Doc("list users").
Metadata(restfulspec.KeyOpenAPITags, tags).
Param(ws.QueryParameter("page", "query the page number").DataType("integer")).
Param(ws.QueryParameter("pageSize", "query the page size number").DataType("integer")).
Param(ws.QueryParameter("name", "fuzzy search based on name").DataType("string")).
Param(ws.QueryParameter("email", "fuzzy search based on email").DataType("string")).
Param(ws.QueryParameter("alias", "fuzzy search based on alias").DataType("string")).
Returns(200, "OK", apis.ListUserResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ListUserResponse{}))
ws.Route(ws.POST("/").To(c.createUser).
Doc("create a user").
Metadata(restfulspec.KeyOpenAPITags, tags).
Reads(apis.CreateUserRequest{}).
Returns(200, "OK", apis.UserBase{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.UserBase{}))
ws.Route(ws.GET("/{username}").To(c.detailUser).
Doc("get user detail").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(c.userCheckFilter).
Returns(200, "OK", apis.DetailUserResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.DetailUserResponse{}))
ws.Route(ws.PUT("/{username}").To(c.updateUser).
Doc("update a user's alias or password").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(c.userCheckFilter).
Returns(200, "OK", apis.UserBase{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.UserBase{}))
ws.Route(ws.DELETE("/{username}").To(c.deleteUser).
Doc("delete a user").
Metadata(restfulspec.KeyOpenAPITags, tags).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.EmptyResponse{}))
ws.Route(ws.GET("/{username}/disable").To(c.disableUser).
Doc("disable a user").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(c.userCheckFilter).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.EmptyResponse{}))
ws.Route(ws.GET("/{username}/enable").To(c.enableUser).
Doc("enable a user").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(c.userCheckFilter).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.EmptyResponse{}))
return ws
}
func (c *userWebService) userCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) {
user, err := c.userUsecase.GetUser(req.Request.Context(), req.PathParameter("username"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyUser, user))
chain.ProcessFilter(req, res)
}
func (c *userWebService) createUser(req *restful.Request, res *restful.Response) {
var createReq apis.CreateUserRequest
if err := req.ReadEntity(&createReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := validate.Struct(&createReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
resp, err := c.userUsecase.CreateUser(req.Request.Context(), createReq)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(resp); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (c *userWebService) detailUser(req *restful.Request, res *restful.Response) {
user := req.Request.Context().Value(&apis.CtxKeyUser).(*model.User)
resp, err := c.userUsecase.DetailUser(req.Request.Context(), user)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(resp); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (c *userWebService) deleteUser(req *restful.Request, res *restful.Response) {
err := c.userUsecase.DeleteUser(req.Request.Context(), req.PathParameter("username"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (c *userWebService) listUser(req *restful.Request, res *restful.Response) {
page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
resp, err := c.userUsecase.ListUsers(req.Request.Context(), page, pageSize, apis.ListUserOptions{
Name: req.QueryParameter("name"),
Alias: req.QueryParameter("alias"),
Email: req.QueryParameter("email"),
})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(resp); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (c *userWebService) updateUser(req *restful.Request, res *restful.Response) {
user := req.Request.Context().Value(&apis.CtxKeyUser).(*model.User)
var updateReq apis.UpdateUserRequest
if err := req.ReadEntity(&updateReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := validate.Struct(&updateReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
resp, err := c.userUsecase.UpdateUser(req.Request.Context(), user, updateReq)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(resp); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (c *userWebService) disableUser(req *restful.Request, res *restful.Response) {
user := req.Request.Context().Value(&apis.CtxKeyUser).(*model.User)
err := c.userUsecase.DisableUser(req.Request.Context(), user)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (c *userWebService) enableUser(req *restful.Request, res *restful.Response) {
user := req.Request.Context().Value(&apis.CtxKeyUser).(*model.User)
err := c.userUsecase.EnableUser(req.Request.Context(), user)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}
}

View File

@ -18,6 +18,7 @@ package webservice
import (
"regexp"
"unicode"
"github.com/go-playground/validator/v10"
@ -26,7 +27,10 @@ import (
var validate = validator.New()
var nameRegexp = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
var (
nameRegexp = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
emailRegexp = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
)
const (
minPageSize = 5
@ -43,6 +47,12 @@ func init() {
if err := validate.RegisterValidation("checkpayloadtype", ValidatePayloadType); err != nil {
panic(err)
}
if err := validate.RegisterValidation("checkemail", ValidateEmail); err != nil {
panic(err)
}
if err := validate.RegisterValidation("checkpassword", ValidatePassword); err != nil {
panic(err)
}
}
// ValidatePayloadType check PayloadType
@ -73,3 +83,35 @@ func ValidateAlias(fl validator.FieldLevel) bool {
}
return true
}
// ValidateEmail custom check email field
func ValidateEmail(fl validator.FieldLevel) bool {
value := fl.Field().String()
if value == "" {
return true
}
return emailRegexp.MatchString(value)
}
// ValidatePassword custom check password field
func ValidatePassword(fl validator.FieldLevel) bool {
value := fl.Field().String()
if value == "" {
return true
}
if len(value) < 8 || len(value) > 16 {
return false
}
// go's regex doesn't support backtracking so check the password with a loop
letter := false
num := false
for _, c := range value {
switch {
case unicode.IsNumber(c):
num = true
case unicode.IsLetter(c):
letter = true
}
}
return letter && num
}

View File

@ -67,4 +67,64 @@ var _ = Describe("Test validate function", func() {
err = validate.Struct(&component)
Expect(err).Should(BeNil())
})
It("Test check email validate ", func() {
invalidEmail := &apisv1.CreateUserRequest{
Name: "user",
Password: "password1",
Email: "invalidEmail",
}
err := validate.Struct(invalidEmail)
Expect(err).ShouldNot(BeNil())
validEmail := &apisv1.CreateUserRequest{
Name: "user",
Password: "password1",
Email: "test@example.com",
}
err = validate.Struct(validEmail)
Expect(err).Should(BeNil())
})
It("Test check password validate ", func() {
invalidPwd := &apisv1.CreateUserRequest{
Name: "user",
Password: "password",
Email: "test@example.com",
}
err := validate.Struct(invalidPwd)
Expect(err).ShouldNot(BeNil())
invalidPwd = &apisv1.CreateUserRequest{
Name: "user",
Password: "a",
Email: "test@example.com",
}
err = validate.Struct(invalidPwd)
Expect(err).ShouldNot(BeNil())
invalidPwd = &apisv1.CreateUserRequest{
Name: "user",
Password: "passwordpasswordpassword",
Email: "test@example.com",
}
err = validate.Struct(invalidPwd)
Expect(err).ShouldNot(BeNil())
invalidPwd = &apisv1.CreateUserRequest{
Name: "user",
Password: "11111111",
Email: "test@example.com",
}
err = validate.Struct(invalidPwd)
Expect(err).ShouldNot(BeNil())
validPwd := &apisv1.CreateUserRequest{
Name: "user",
Password: "password1",
Email: "test@example.com",
}
err = validate.Struct(validPwd)
Expect(err).Should(BeNil())
})
})

View File

@ -75,6 +75,7 @@ func Init(ds datastore.DataStore, addonCacheTime time.Duration) {
systemInfoUsecase := usecase.NewSystemInfoUsecase(ds)
helmUsecase := usecase.NewHelmUsecase()
authenticationUsecase := usecase.NewAuthenticationUsecase(ds, systemInfoUsecase)
userUsecase := usecase.NewUserUsecase(ds, projectUsecase, systemInfoUsecase)
// init for default values
@ -100,6 +101,7 @@ func Init(ds datastore.DataStore, addonCacheTime time.Duration) {
// Authentication
RegisterWebService(NewAuthenticationWebService(authenticationUsecase))
RegisterWebService(NewUserWebService(userUsecase))
RegisterWebService(NewSystemInfoWebService(systemInfoUsecase))

View File

@ -0,0 +1,124 @@
/*
Copyright 2022 The KubeVela 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 e2e_apiserver_test
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
)
const (
username = "my-user"
urlPrefix = "http://127.0.0.1:8000/api/v1/users"
)
var _ = Describe("Test user rest api", func() {
It("Test create user", func() {
defer GinkgoRecover()
var req = apisv1.CreateUserRequest{
Name: username,
Alias: "alias",
Email: "test@example.com",
Password: "password1",
}
bodyByte, err := json.Marshal(req)
Expect(err).Should(BeNil())
res, err := http.Post(urlPrefix, "application/json", bytes.NewBuffer(bodyByte))
Expect(err).Should(BeNil())
Expect(res).ShouldNot(BeNil())
Expect(res.StatusCode).Should(Equal(200))
Expect(res.Body).ShouldNot(BeNil())
defer res.Body.Close()
userBase := &apisv1.UserBase{}
err = json.NewDecoder(res.Body).Decode(&userBase)
Expect(err).Should(BeNil())
Expect(userBase.Name).Should(Equal(username))
Expect(userBase.Alias).Should(Equal("alias"))
Expect(userBase.Email).Should(Equal("test@example.com"))
})
It("Test list users", func() {
defer GinkgoRecover()
res, err := http.Get(urlPrefix)
Expect(err).Should(BeNil())
Expect(res).ShouldNot(BeNil())
Expect(res.StatusCode).Should(Equal(200))
Expect(res.Body).ShouldNot(BeNil())
defer res.Body.Close()
users := &apisv1.ListUserResponse{}
err = json.NewDecoder(res.Body).Decode(users)
Expect(err).Should(BeNil())
Expect(users.Total).Should(Equal(int64(1)))
})
It("Test detail user", func() {
defer GinkgoRecover()
res, err := http.Get(fmt.Sprintf("%s/%s", urlPrefix, username))
Expect(err).Should(BeNil())
Expect(res).ShouldNot(BeNil())
Expect(res.StatusCode).Should(Equal(200))
Expect(res.Body).ShouldNot(BeNil())
defer res.Body.Close()
var detail apisv1.DetailUserResponse
err = json.NewDecoder(res.Body).Decode(&detail)
Expect(err).Should(BeNil())
Expect(detail.Name).Should(Equal(username))
Expect(detail.Alias).Should(Equal("alias"))
Expect(detail.Email).Should(Equal("test@example.com"))
Expect(len(detail.Projects)).Should(Equal(0))
})
It("Test update user", func() {
defer GinkgoRecover()
var updateReq = apisv1.UpdateUserRequest{
Alias: "updated-alias",
}
bodyByte, err := json.Marshal(updateReq)
Expect(err).Should(BeNil())
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", urlPrefix, username), bytes.NewBuffer(bodyByte))
Expect(err).Should(BeNil())
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
Expect(err).Should(BeNil())
Expect(res).ShouldNot(BeNil())
Expect(res.StatusCode).Should(Equal(200))
Expect(res.Body).ShouldNot(BeNil())
defer res.Body.Close()
userBase := &apisv1.UserBase{}
err = json.NewDecoder(res.Body).Decode(&userBase)
Expect(err).Should(BeNil())
Expect(userBase.Alias).Should(Equal("updated-alias"))
})
It("Test delete user", func() {
defer GinkgoRecover()
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%s", urlPrefix, username), nil)
Expect(err).Should(BeNil())
res, err := http.DefaultClient.Do(req)
Expect(err).Should(BeNil())
Expect(res).ShouldNot(BeNil())
Expect(res.StatusCode).Should(Equal(200))
})
})