diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1c93f99..f65bc57b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## v0.4.25 +FEATURE: New action in the web console that allows changing the password of the logged-in account (https://github.com/openziti/zrok/issues/148) + FEATURE: The web console now supports revoking your current account token and generating a new one (https://github.com/openziti/zrok/issues/191) CHANGE: When specifying OAuth configuration for public shares from the `zrok share public` or `zrok reserve` public commands, the flags and functionality for restricting the allowed email addresses of the authenticating users has changed. The old flag was `--oauth-email-domains`, which took a string value that needed to be contained in the user's email address. The new flag is `--oauth-email-address-patterns`, which accepts a glob-style filter, using https://github.com/gobwas/glob (https://github.com/openziti/zrok/issues/413) diff --git a/controller/changePassword.go b/controller/changePassword.go new file mode 100644 index 000000000..ab05a494e --- /dev/null +++ b/controller/changePassword.go @@ -0,0 +1,75 @@ +package controller + +import ( + "github.com/go-openapi/runtime/middleware" + "github.com/openziti/zrok/controller/config" + "github.com/openziti/zrok/rest_model_zrok" + "github.com/openziti/zrok/rest_server_zrok/operations/account" + "github.com/sirupsen/logrus" +) + +type changePasswordHandler struct { + cfg *config.Config +} + +func newChangePasswordHandler(cfg *config.Config) *changePasswordHandler { + return &changePasswordHandler{ + cfg: cfg, + } +} + +func (handler *changePasswordHandler) Handle(params account.ChangePasswordParams, principal *rest_model_zrok.Principal) middleware.Responder { + if params.Body == nil || params.Body.Email == "" || params.Body.OldPassword == "" || params.Body.NewPassword == "" { + logrus.Error("missing email, old, or new password") + return account.NewChangePasswordUnauthorized() + } + logrus.Infof("received change password request for email '%v'", params.Body.Email) + + tx, err := str.Begin() + if err != nil { + logrus.Errorf("error starting transaction: %v", err) + return account.NewChangePasswordUnauthorized() + } + defer func() { _ = tx.Rollback() }() + + a, err := str.FindAccountWithEmail(params.Body.Email, tx) + if err != nil { + logrus.Errorf("error finding account '%v': %v", params.Body.Email, err) + return account.NewChangePasswordUnauthorized() + } + ohpwd, err := rehashPassword(params.Body.OldPassword, a.Salt) + if err != nil { + logrus.Errorf("error hashing password for '%v': %v", params.Body.Email, err) + return account.NewChangePasswordUnauthorized() + } + if a.Password != ohpwd.Password { + logrus.Errorf("password mismatch for account '%v'", params.Body.Email) + return account.NewChangePasswordUnauthorized() + } + + if err := validatePassword(handler.cfg, params.Body.NewPassword); err != nil { + logrus.Errorf("password not valid for request '%v': %v", a.Email, err) + return account.NewChangePasswordUnprocessableEntity().WithPayload(rest_model_zrok.ErrorMessage(err.Error())) + } + + nhpwd, err := HashPassword(params.Body.NewPassword) + if err != nil { + logrus.Errorf("error hashing password for '%v': %v", a.Email, err) + return account.NewChangePasswordInternalServerError() + } + a.Salt = nhpwd.Salt + a.Password = nhpwd.Password + + if _, err := str.UpdateAccount(a, tx); err != nil { + logrus.Errorf("error updating for '%v': %v", a.Email, err) + return account.NewChangePasswordInternalServerError() + } + + if err := tx.Commit(); err != nil { + logrus.Errorf("error committing '%v': %v", a.Email, err) + return account.NewChangePasswordInternalServerError() + } + + logrus.Infof("change password for '%v'", a.Email) + return account.NewChangePasswordOK() +} diff --git a/controller/controller.go b/controller/controller.go index c30d86257..e706749a2 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -44,6 +44,7 @@ func Run(inCfg *config.Config) error { api := operations.NewZrokAPI(swaggerSpec) api.KeyAuth = newZrokAuthenticator(cfg).authenticate + api.AccountChangePasswordHandler = newChangePasswordHandler(cfg) api.AccountInviteHandler = newInviteHandler(cfg) api.AccountLoginHandler = account.LoginHandlerFunc(loginHandler) api.AccountRegisterHandler = newRegisterHandler(cfg) diff --git a/controller/resetToken.go b/controller/resetToken.go index 38d9dfa3e..f00a3ba34 100644 --- a/controller/resetToken.go +++ b/controller/resetToken.go @@ -14,11 +14,12 @@ func newResetTokenHandler() *resetTokenHandler { } func (handler *resetTokenHandler) Handle(params account.ResetTokenParams, principal *rest_model_zrok.Principal) middleware.Responder { - if params.Body.EmailAddress == "" { - logrus.Error("missing email") + logrus.Infof("received token regeneration request for email '%v'", principal.Email) + + if params.Body.EmailAddress != principal.Email { + logrus.Errorf("mismatched account '%v' for '%v'", params.Body.EmailAddress, principal.Email) return account.NewResetTokenNotFound() } - logrus.Infof("received token reset request for email '%v'", params.Body.EmailAddress) tx, err := str.Begin() if err != nil { @@ -56,7 +57,7 @@ func (handler *resetTokenHandler) Handle(params account.ResetTokenParams, princi return account.NewResetTokenInternalServerError() } - logrus.Infof("reset token for '%v'", a.Email) + logrus.Infof("regenerated token '%v' for '%v'", a.Token, a.Email) return account.NewResetTokenOK().WithPayload(&account.ResetTokenOKBody{Token: token}) } diff --git a/rest_client_zrok/account/account_client.go b/rest_client_zrok/account/account_client.go index 83049a8b8..17323ed50 100644 --- a/rest_client_zrok/account/account_client.go +++ b/rest_client_zrok/account/account_client.go @@ -30,6 +30,8 @@ type ClientOption func(*runtime.ClientOperation) // ClientService is the interface for Client methods type ClientService interface { + ChangePassword(params *ChangePasswordParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ChangePasswordOK, error) + Invite(params *InviteParams, opts ...ClientOption) (*InviteCreated, error) Login(params *LoginParams, opts ...ClientOption) (*LoginOK, error) @@ -47,6 +49,45 @@ type ClientService interface { SetTransport(transport runtime.ClientTransport) } +/* +ChangePassword change password API +*/ +func (a *Client) ChangePassword(params *ChangePasswordParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ChangePasswordOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewChangePasswordParams() + } + op := &runtime.ClientOperation{ + ID: "changePassword", + Method: "POST", + PathPattern: "/changePassword", + ProducesMediaTypes: []string{"application/zrok.v1+json"}, + ConsumesMediaTypes: []string{"application/zrok.v1+json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &ChangePasswordReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*ChangePasswordOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for changePassword: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* Invite invite API */ diff --git a/rest_client_zrok/account/change_password_parameters.go b/rest_client_zrok/account/change_password_parameters.go new file mode 100644 index 000000000..8ab0d86c1 --- /dev/null +++ b/rest_client_zrok/account/change_password_parameters.go @@ -0,0 +1,150 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package account + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + "github.com/openziti/zrok/rest_model_zrok" +) + +// NewChangePasswordParams creates a new ChangePasswordParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewChangePasswordParams() *ChangePasswordParams { + return &ChangePasswordParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewChangePasswordParamsWithTimeout creates a new ChangePasswordParams object +// with the ability to set a timeout on a request. +func NewChangePasswordParamsWithTimeout(timeout time.Duration) *ChangePasswordParams { + return &ChangePasswordParams{ + timeout: timeout, + } +} + +// NewChangePasswordParamsWithContext creates a new ChangePasswordParams object +// with the ability to set a context for a request. +func NewChangePasswordParamsWithContext(ctx context.Context) *ChangePasswordParams { + return &ChangePasswordParams{ + Context: ctx, + } +} + +// NewChangePasswordParamsWithHTTPClient creates a new ChangePasswordParams object +// with the ability to set a custom HTTPClient for a request. +func NewChangePasswordParamsWithHTTPClient(client *http.Client) *ChangePasswordParams { + return &ChangePasswordParams{ + HTTPClient: client, + } +} + +/* +ChangePasswordParams contains all the parameters to send to the API endpoint + + for the change password operation. + + Typically these are written to a http.Request. +*/ +type ChangePasswordParams struct { + + // Body. + Body *rest_model_zrok.ChangePasswordRequest + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the change password params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *ChangePasswordParams) WithDefaults() *ChangePasswordParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the change password params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *ChangePasswordParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the change password params +func (o *ChangePasswordParams) WithTimeout(timeout time.Duration) *ChangePasswordParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the change password params +func (o *ChangePasswordParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the change password params +func (o *ChangePasswordParams) WithContext(ctx context.Context) *ChangePasswordParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the change password params +func (o *ChangePasswordParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the change password params +func (o *ChangePasswordParams) WithHTTPClient(client *http.Client) *ChangePasswordParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the change password params +func (o *ChangePasswordParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the change password params +func (o *ChangePasswordParams) WithBody(body *rest_model_zrok.ChangePasswordRequest) *ChangePasswordParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the change password params +func (o *ChangePasswordParams) SetBody(body *rest_model_zrok.ChangePasswordRequest) { + o.Body = body +} + +// WriteToRequest writes these params to a swagger request +func (o *ChangePasswordParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if o.Body != nil { + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/rest_client_zrok/account/change_password_responses.go b/rest_client_zrok/account/change_password_responses.go new file mode 100644 index 000000000..195afe8be --- /dev/null +++ b/rest_client_zrok/account/change_password_responses.go @@ -0,0 +1,349 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package account + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/openziti/zrok/rest_model_zrok" +) + +// ChangePasswordReader is a Reader for the ChangePassword structure. +type ChangePasswordReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *ChangePasswordReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewChangePasswordOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewChangePasswordBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 401: + result := NewChangePasswordUnauthorized() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 422: + result := NewChangePasswordUnprocessableEntity() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewChangePasswordInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[POST /changePassword] changePassword", response, response.Code()) + } +} + +// NewChangePasswordOK creates a ChangePasswordOK with default headers values +func NewChangePasswordOK() *ChangePasswordOK { + return &ChangePasswordOK{} +} + +/* +ChangePasswordOK describes a response with status code 200, with default header values. + +changed password +*/ +type ChangePasswordOK struct { +} + +// IsSuccess returns true when this change password o k response has a 2xx status code +func (o *ChangePasswordOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this change password o k response has a 3xx status code +func (o *ChangePasswordOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this change password o k response has a 4xx status code +func (o *ChangePasswordOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this change password o k response has a 5xx status code +func (o *ChangePasswordOK) IsServerError() bool { + return false +} + +// IsCode returns true when this change password o k response a status code equal to that given +func (o *ChangePasswordOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the change password o k response +func (o *ChangePasswordOK) Code() int { + return 200 +} + +func (o *ChangePasswordOK) Error() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordOK ", 200) +} + +func (o *ChangePasswordOK) String() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordOK ", 200) +} + +func (o *ChangePasswordOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + return nil +} + +// NewChangePasswordBadRequest creates a ChangePasswordBadRequest with default headers values +func NewChangePasswordBadRequest() *ChangePasswordBadRequest { + return &ChangePasswordBadRequest{} +} + +/* +ChangePasswordBadRequest describes a response with status code 400, with default header values. + +password not changed +*/ +type ChangePasswordBadRequest struct { +} + +// IsSuccess returns true when this change password bad request response has a 2xx status code +func (o *ChangePasswordBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this change password bad request response has a 3xx status code +func (o *ChangePasswordBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this change password bad request response has a 4xx status code +func (o *ChangePasswordBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this change password bad request response has a 5xx status code +func (o *ChangePasswordBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this change password bad request response a status code equal to that given +func (o *ChangePasswordBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the change password bad request response +func (o *ChangePasswordBadRequest) Code() int { + return 400 +} + +func (o *ChangePasswordBadRequest) Error() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordBadRequest ", 400) +} + +func (o *ChangePasswordBadRequest) String() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordBadRequest ", 400) +} + +func (o *ChangePasswordBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + return nil +} + +// NewChangePasswordUnauthorized creates a ChangePasswordUnauthorized with default headers values +func NewChangePasswordUnauthorized() *ChangePasswordUnauthorized { + return &ChangePasswordUnauthorized{} +} + +/* +ChangePasswordUnauthorized describes a response with status code 401, with default header values. + +unauthorized +*/ +type ChangePasswordUnauthorized struct { +} + +// IsSuccess returns true when this change password unauthorized response has a 2xx status code +func (o *ChangePasswordUnauthorized) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this change password unauthorized response has a 3xx status code +func (o *ChangePasswordUnauthorized) IsRedirect() bool { + return false +} + +// IsClientError returns true when this change password unauthorized response has a 4xx status code +func (o *ChangePasswordUnauthorized) IsClientError() bool { + return true +} + +// IsServerError returns true when this change password unauthorized response has a 5xx status code +func (o *ChangePasswordUnauthorized) IsServerError() bool { + return false +} + +// IsCode returns true when this change password unauthorized response a status code equal to that given +func (o *ChangePasswordUnauthorized) IsCode(code int) bool { + return code == 401 +} + +// Code gets the status code for the change password unauthorized response +func (o *ChangePasswordUnauthorized) Code() int { + return 401 +} + +func (o *ChangePasswordUnauthorized) Error() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordUnauthorized ", 401) +} + +func (o *ChangePasswordUnauthorized) String() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordUnauthorized ", 401) +} + +func (o *ChangePasswordUnauthorized) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + return nil +} + +// NewChangePasswordUnprocessableEntity creates a ChangePasswordUnprocessableEntity with default headers values +func NewChangePasswordUnprocessableEntity() *ChangePasswordUnprocessableEntity { + return &ChangePasswordUnprocessableEntity{} +} + +/* +ChangePasswordUnprocessableEntity describes a response with status code 422, with default header values. + +password validation failure +*/ +type ChangePasswordUnprocessableEntity struct { + Payload rest_model_zrok.ErrorMessage +} + +// IsSuccess returns true when this change password unprocessable entity response has a 2xx status code +func (o *ChangePasswordUnprocessableEntity) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this change password unprocessable entity response has a 3xx status code +func (o *ChangePasswordUnprocessableEntity) IsRedirect() bool { + return false +} + +// IsClientError returns true when this change password unprocessable entity response has a 4xx status code +func (o *ChangePasswordUnprocessableEntity) IsClientError() bool { + return true +} + +// IsServerError returns true when this change password unprocessable entity response has a 5xx status code +func (o *ChangePasswordUnprocessableEntity) IsServerError() bool { + return false +} + +// IsCode returns true when this change password unprocessable entity response a status code equal to that given +func (o *ChangePasswordUnprocessableEntity) IsCode(code int) bool { + return code == 422 +} + +// Code gets the status code for the change password unprocessable entity response +func (o *ChangePasswordUnprocessableEntity) Code() int { + return 422 +} + +func (o *ChangePasswordUnprocessableEntity) Error() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordUnprocessableEntity %+v", 422, o.Payload) +} + +func (o *ChangePasswordUnprocessableEntity) String() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordUnprocessableEntity %+v", 422, o.Payload) +} + +func (o *ChangePasswordUnprocessableEntity) GetPayload() rest_model_zrok.ErrorMessage { + return o.Payload +} + +func (o *ChangePasswordUnprocessableEntity) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewChangePasswordInternalServerError creates a ChangePasswordInternalServerError with default headers values +func NewChangePasswordInternalServerError() *ChangePasswordInternalServerError { + return &ChangePasswordInternalServerError{} +} + +/* +ChangePasswordInternalServerError describes a response with status code 500, with default header values. + +internal server error +*/ +type ChangePasswordInternalServerError struct { +} + +// IsSuccess returns true when this change password internal server error response has a 2xx status code +func (o *ChangePasswordInternalServerError) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this change password internal server error response has a 3xx status code +func (o *ChangePasswordInternalServerError) IsRedirect() bool { + return false +} + +// IsClientError returns true when this change password internal server error response has a 4xx status code +func (o *ChangePasswordInternalServerError) IsClientError() bool { + return false +} + +// IsServerError returns true when this change password internal server error response has a 5xx status code +func (o *ChangePasswordInternalServerError) IsServerError() bool { + return true +} + +// IsCode returns true when this change password internal server error response a status code equal to that given +func (o *ChangePasswordInternalServerError) IsCode(code int) bool { + return code == 500 +} + +// Code gets the status code for the change password internal server error response +func (o *ChangePasswordInternalServerError) Code() int { + return 500 +} + +func (o *ChangePasswordInternalServerError) Error() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordInternalServerError ", 500) +} + +func (o *ChangePasswordInternalServerError) String() string { + return fmt.Sprintf("[POST /changePassword][%d] changePasswordInternalServerError ", 500) +} + +func (o *ChangePasswordInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + return nil +} diff --git a/rest_model_zrok/change_password_request.go b/rest_model_zrok/change_password_request.go new file mode 100644 index 000000000..962219d15 --- /dev/null +++ b/rest_model_zrok/change_password_request.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package rest_model_zrok + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// ChangePasswordRequest change password request +// +// swagger:model changePasswordRequest +type ChangePasswordRequest struct { + + // email + Email string `json:"email,omitempty"` + + // new password + NewPassword string `json:"newPassword,omitempty"` + + // old password + OldPassword string `json:"oldPassword,omitempty"` +} + +// Validate validates this change password request +func (m *ChangePasswordRequest) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this change password request based on context it is used +func (m *ChangePasswordRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *ChangePasswordRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ChangePasswordRequest) UnmarshalBinary(b []byte) error { + var res ChangePasswordRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/rest_server_zrok/embedded_spec.go b/rest_server_zrok/embedded_spec.go index f04c18d01..0997d6703 100644 --- a/rest_server_zrok/embedded_spec.go +++ b/rest_server_zrok/embedded_spec.go @@ -74,6 +74,48 @@ func init() { } } }, + "/changePassword": { + "post": { + "security": [ + { + "key": [] + } + ], + "tags": [ + "account" + ], + "operationId": "changePassword", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/changePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "changed password" + }, + "400": { + "description": "password not changed" + }, + "401": { + "description": "unauthorized" + }, + "422": { + "description": "password validation failure", + "schema": { + "$ref": "#/definitions/errorMessage" + } + }, + "500": { + "description": "internal server error" + } + } + } + }, "/configuration": { "get": { "tags": [ @@ -1115,6 +1157,20 @@ func init() { } } }, + "changePasswordRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "newPassword": { + "type": "string" + }, + "oldPassword": { + "type": "string" + } + } + }, "configuration": { "type": "object", "properties": { @@ -1745,6 +1801,48 @@ func init() { } } }, + "/changePassword": { + "post": { + "security": [ + { + "key": [] + } + ], + "tags": [ + "account" + ], + "operationId": "changePassword", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/changePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "changed password" + }, + "400": { + "description": "password not changed" + }, + "401": { + "description": "unauthorized" + }, + "422": { + "description": "password validation failure", + "schema": { + "$ref": "#/definitions/errorMessage" + } + }, + "500": { + "description": "internal server error" + } + } + } + }, "/configuration": { "get": { "tags": [ @@ -2786,6 +2884,20 @@ func init() { } } }, + "changePasswordRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "newPassword": { + "type": "string" + }, + "oldPassword": { + "type": "string" + } + } + }, "configuration": { "type": "object", "properties": { diff --git a/rest_server_zrok/operations/account/change_password.go b/rest_server_zrok/operations/account/change_password.go new file mode 100644 index 000000000..5324b58e7 --- /dev/null +++ b/rest_server_zrok/operations/account/change_password.go @@ -0,0 +1,71 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package account + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" + + "github.com/openziti/zrok/rest_model_zrok" +) + +// ChangePasswordHandlerFunc turns a function with the right signature into a change password handler +type ChangePasswordHandlerFunc func(ChangePasswordParams, *rest_model_zrok.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn ChangePasswordHandlerFunc) Handle(params ChangePasswordParams, principal *rest_model_zrok.Principal) middleware.Responder { + return fn(params, principal) +} + +// ChangePasswordHandler interface for that can handle valid change password params +type ChangePasswordHandler interface { + Handle(ChangePasswordParams, *rest_model_zrok.Principal) middleware.Responder +} + +// NewChangePassword creates a new http.Handler for the change password operation +func NewChangePassword(ctx *middleware.Context, handler ChangePasswordHandler) *ChangePassword { + return &ChangePassword{Context: ctx, Handler: handler} +} + +/* + ChangePassword swagger:route POST /changePassword account changePassword + +ChangePassword change password API +*/ +type ChangePassword struct { + Context *middleware.Context + Handler ChangePasswordHandler +} + +func (o *ChangePassword) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewChangePasswordParams() + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + *r = *aCtx + } + var principal *rest_model_zrok.Principal + if uprinc != nil { + principal = uprinc.(*rest_model_zrok.Principal) // this is really a rest_model_zrok.Principal, I promise + } + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params, principal) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/rest_server_zrok/operations/account/change_password_parameters.go b/rest_server_zrok/operations/account/change_password_parameters.go new file mode 100644 index 000000000..5afcfb6e0 --- /dev/null +++ b/rest_server_zrok/operations/account/change_password_parameters.go @@ -0,0 +1,76 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package account + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/validate" + + "github.com/openziti/zrok/rest_model_zrok" +) + +// NewChangePasswordParams creates a new ChangePasswordParams object +// +// There are no default values defined in the spec. +func NewChangePasswordParams() ChangePasswordParams { + + return ChangePasswordParams{} +} + +// ChangePasswordParams contains all the bound params for the change password operation +// typically these are obtained from a http.Request +// +// swagger:parameters changePassword +type ChangePasswordParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + In: body + */ + Body *rest_model_zrok.ChangePasswordRequest +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewChangePasswordParams() beforehand. +func (o *ChangePasswordParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body rest_model_zrok.ChangePasswordRequest + if err := route.Consumer.Consume(r.Body, &body); err != nil { + res = append(res, errors.NewParseError("body", "body", "", err)) + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Body = &body + } + } + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/rest_server_zrok/operations/account/change_password_responses.go b/rest_server_zrok/operations/account/change_password_responses.go new file mode 100644 index 000000000..020b9d640 --- /dev/null +++ b/rest_server_zrok/operations/account/change_password_responses.go @@ -0,0 +1,157 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package account + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/openziti/zrok/rest_model_zrok" +) + +// ChangePasswordOKCode is the HTTP code returned for type ChangePasswordOK +const ChangePasswordOKCode int = 200 + +/* +ChangePasswordOK changed password + +swagger:response changePasswordOK +*/ +type ChangePasswordOK struct { +} + +// NewChangePasswordOK creates ChangePasswordOK with default headers values +func NewChangePasswordOK() *ChangePasswordOK { + + return &ChangePasswordOK{} +} + +// WriteResponse to the client +func (o *ChangePasswordOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(200) +} + +// ChangePasswordBadRequestCode is the HTTP code returned for type ChangePasswordBadRequest +const ChangePasswordBadRequestCode int = 400 + +/* +ChangePasswordBadRequest password not changed + +swagger:response changePasswordBadRequest +*/ +type ChangePasswordBadRequest struct { +} + +// NewChangePasswordBadRequest creates ChangePasswordBadRequest with default headers values +func NewChangePasswordBadRequest() *ChangePasswordBadRequest { + + return &ChangePasswordBadRequest{} +} + +// WriteResponse to the client +func (o *ChangePasswordBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(400) +} + +// ChangePasswordUnauthorizedCode is the HTTP code returned for type ChangePasswordUnauthorized +const ChangePasswordUnauthorizedCode int = 401 + +/* +ChangePasswordUnauthorized unauthorized + +swagger:response changePasswordUnauthorized +*/ +type ChangePasswordUnauthorized struct { +} + +// NewChangePasswordUnauthorized creates ChangePasswordUnauthorized with default headers values +func NewChangePasswordUnauthorized() *ChangePasswordUnauthorized { + + return &ChangePasswordUnauthorized{} +} + +// WriteResponse to the client +func (o *ChangePasswordUnauthorized) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(401) +} + +// ChangePasswordUnprocessableEntityCode is the HTTP code returned for type ChangePasswordUnprocessableEntity +const ChangePasswordUnprocessableEntityCode int = 422 + +/* +ChangePasswordUnprocessableEntity password validation failure + +swagger:response changePasswordUnprocessableEntity +*/ +type ChangePasswordUnprocessableEntity struct { + + /* + In: Body + */ + Payload rest_model_zrok.ErrorMessage `json:"body,omitempty"` +} + +// NewChangePasswordUnprocessableEntity creates ChangePasswordUnprocessableEntity with default headers values +func NewChangePasswordUnprocessableEntity() *ChangePasswordUnprocessableEntity { + + return &ChangePasswordUnprocessableEntity{} +} + +// WithPayload adds the payload to the change password unprocessable entity response +func (o *ChangePasswordUnprocessableEntity) WithPayload(payload rest_model_zrok.ErrorMessage) *ChangePasswordUnprocessableEntity { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the change password unprocessable entity response +func (o *ChangePasswordUnprocessableEntity) SetPayload(payload rest_model_zrok.ErrorMessage) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ChangePasswordUnprocessableEntity) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(422) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// ChangePasswordInternalServerErrorCode is the HTTP code returned for type ChangePasswordInternalServerError +const ChangePasswordInternalServerErrorCode int = 500 + +/* +ChangePasswordInternalServerError internal server error + +swagger:response changePasswordInternalServerError +*/ +type ChangePasswordInternalServerError struct { +} + +// NewChangePasswordInternalServerError creates ChangePasswordInternalServerError with default headers values +func NewChangePasswordInternalServerError() *ChangePasswordInternalServerError { + + return &ChangePasswordInternalServerError{} +} + +// WriteResponse to the client +func (o *ChangePasswordInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(500) +} diff --git a/rest_server_zrok/operations/account/change_password_urlbuilder.go b/rest_server_zrok/operations/account/change_password_urlbuilder.go new file mode 100644 index 000000000..1601a8a74 --- /dev/null +++ b/rest_server_zrok/operations/account/change_password_urlbuilder.go @@ -0,0 +1,87 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package account + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// ChangePasswordURL generates an URL for the change password operation +type ChangePasswordURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *ChangePasswordURL) WithBasePath(bp string) *ChangePasswordURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *ChangePasswordURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *ChangePasswordURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/changePassword" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *ChangePasswordURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *ChangePasswordURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *ChangePasswordURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on ChangePasswordURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on ChangePasswordURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *ChangePasswordURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/rest_server_zrok/operations/zrok_api.go b/rest_server_zrok/operations/zrok_api.go index fa03ad3ec..8de85d53b 100644 --- a/rest_server_zrok/operations/zrok_api.go +++ b/rest_server_zrok/operations/zrok_api.go @@ -52,6 +52,9 @@ func NewZrokAPI(spec *loads.Document) *ZrokAPI { ShareAccessHandler: share.AccessHandlerFunc(func(params share.AccessParams, principal *rest_model_zrok.Principal) middleware.Responder { return middleware.NotImplemented("operation share.Access has not yet been implemented") }), + AccountChangePasswordHandler: account.ChangePasswordHandlerFunc(func(params account.ChangePasswordParams, principal *rest_model_zrok.Principal) middleware.Responder { + return middleware.NotImplemented("operation account.ChangePassword has not yet been implemented") + }), MetadataConfigurationHandler: metadata.ConfigurationHandlerFunc(func(params metadata.ConfigurationParams) middleware.Responder { return middleware.NotImplemented("operation metadata.Configuration has not yet been implemented") }), @@ -191,6 +194,8 @@ type ZrokAPI struct { // ShareAccessHandler sets the operation handler for the access operation ShareAccessHandler share.AccessHandler + // AccountChangePasswordHandler sets the operation handler for the change password operation + AccountChangePasswordHandler account.ChangePasswordHandler // MetadataConfigurationHandler sets the operation handler for the configuration operation MetadataConfigurationHandler metadata.ConfigurationHandler // AdminCreateFrontendHandler sets the operation handler for the create frontend operation @@ -333,6 +338,9 @@ func (o *ZrokAPI) Validate() error { if o.ShareAccessHandler == nil { unregistered = append(unregistered, "share.AccessHandler") } + if o.AccountChangePasswordHandler == nil { + unregistered = append(unregistered, "account.ChangePasswordHandler") + } if o.MetadataConfigurationHandler == nil { unregistered = append(unregistered, "metadata.ConfigurationHandler") } @@ -523,6 +531,10 @@ func (o *ZrokAPI) initHandlerCache() { o.handlers["POST"] = make(map[string]http.Handler) } o.handlers["POST"]["/access"] = share.NewAccess(o.context, o.ShareAccessHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } + o.handlers["POST"]["/changePassword"] = account.NewChangePassword(o.context, o.AccountChangePasswordHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } diff --git a/sdk/python/sdk/zrok/zrok_api/__init__.py b/sdk/python/sdk/zrok/zrok_api/__init__.py index 158ae3cf2..87130c7ca 100644 --- a/sdk/python/sdk/zrok/zrok_api/__init__.py +++ b/sdk/python/sdk/zrok/zrok_api/__init__.py @@ -27,6 +27,7 @@ from zrok_api.models.access_request import AccessRequest from zrok_api.models.access_response import AccessResponse from zrok_api.models.auth_user import AuthUser +from zrok_api.models.change_password_request import ChangePasswordRequest from zrok_api.models.configuration import Configuration from zrok_api.models.create_frontend_request import CreateFrontendRequest from zrok_api.models.create_frontend_response import CreateFrontendResponse diff --git a/sdk/python/sdk/zrok/zrok_api/api/account_api.py b/sdk/python/sdk/zrok/zrok_api/api/account_api.py index 77e0528bb..dac28edb9 100644 --- a/sdk/python/sdk/zrok/zrok_api/api/account_api.py +++ b/sdk/python/sdk/zrok/zrok_api/api/account_api.py @@ -32,6 +32,99 @@ def __init__(self, api_client=None): api_client = ApiClient() self.api_client = api_client + def change_password(self, **kwargs): # noqa: E501 + """change_password # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.change_password(async_req=True) + >>> result = thread.get() + + :param async_req bool + :param ChangePasswordRequest body: + :return: None + If the method is called asynchronously, + returns the request thread. + """ + kwargs['_return_http_data_only'] = True + if kwargs.get('async_req'): + return self.change_password_with_http_info(**kwargs) # noqa: E501 + else: + (data) = self.change_password_with_http_info(**kwargs) # noqa: E501 + return data + + def change_password_with_http_info(self, **kwargs): # noqa: E501 + """change_password # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.change_password_with_http_info(async_req=True) + >>> result = thread.get() + + :param async_req bool + :param ChangePasswordRequest body: + :return: None + If the method is called asynchronously, + returns the request thread. + """ + + all_params = ['body'] # noqa: E501 + all_params.append('async_req') + all_params.append('_return_http_data_only') + all_params.append('_preload_content') + all_params.append('_request_timeout') + + params = locals() + for key, val in six.iteritems(params['kwargs']): + if key not in all_params: + raise TypeError( + "Got an unexpected keyword argument '%s'" + " to method change_password" % key + ) + params[key] = val + del params['kwargs'] + + collection_formats = {} + + path_params = {} + + query_params = [] + + header_params = {} + + form_params = [] + local_var_files = {} + + body_params = None + if 'body' in params: + body_params = params['body'] + # HTTP header `Accept` + header_params['Accept'] = self.api_client.select_header_accept( + ['application/zrok.v1+json']) # noqa: E501 + + # HTTP header `Content-Type` + header_params['Content-Type'] = self.api_client.select_header_content_type( # noqa: E501 + ['application/zrok.v1+json']) # noqa: E501 + + # Authentication setting + auth_settings = ['key'] # noqa: E501 + + return self.api_client.call_api( + '/changePassword', 'POST', + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_type=None, # noqa: E501 + auth_settings=auth_settings, + async_req=params.get('async_req'), + _return_http_data_only=params.get('_return_http_data_only'), + _preload_content=params.get('_preload_content', True), + _request_timeout=params.get('_request_timeout'), + collection_formats=collection_formats) + def invite(self, **kwargs): # noqa: E501 """invite # noqa: E501 diff --git a/sdk/python/sdk/zrok/zrok_api/models/__init__.py b/sdk/python/sdk/zrok/zrok_api/models/__init__.py index 71b0fa1a8..b792df015 100644 --- a/sdk/python/sdk/zrok/zrok_api/models/__init__.py +++ b/sdk/python/sdk/zrok/zrok_api/models/__init__.py @@ -17,6 +17,7 @@ from zrok_api.models.access_request import AccessRequest from zrok_api.models.access_response import AccessResponse from zrok_api.models.auth_user import AuthUser +from zrok_api.models.change_password_request import ChangePasswordRequest from zrok_api.models.configuration import Configuration from zrok_api.models.create_frontend_request import CreateFrontendRequest from zrok_api.models.create_frontend_response import CreateFrontendResponse diff --git a/sdk/python/sdk/zrok/zrok_api/models/change_password_request.py b/sdk/python/sdk/zrok/zrok_api/models/change_password_request.py new file mode 100644 index 000000000..fe3f48626 --- /dev/null +++ b/sdk/python/sdk/zrok/zrok_api/models/change_password_request.py @@ -0,0 +1,162 @@ +# coding: utf-8 + +""" + zrok + + zrok client access # noqa: E501 + + OpenAPI spec version: 0.3.0 + + Generated by: https://github.com/swagger-api/swagger-codegen.git +""" + +import pprint +import re # noqa: F401 + +import six + +class ChangePasswordRequest(object): + """NOTE: This class is auto generated by the swagger code generator program. + + Do not edit the class manually. + """ + """ + Attributes: + swagger_types (dict): The key is attribute name + and the value is attribute type. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + """ + swagger_types = { + 'email': 'str', + 'old_password': 'str', + 'new_password': 'str' + } + + attribute_map = { + 'email': 'email', + 'old_password': 'oldPassword', + 'new_password': 'newPassword' + } + + def __init__(self, email=None, old_password=None, new_password=None): # noqa: E501 + """ChangePasswordRequest - a model defined in Swagger""" # noqa: E501 + self._email = None + self._old_password = None + self._new_password = None + self.discriminator = None + if email is not None: + self.email = email + if old_password is not None: + self.old_password = old_password + if new_password is not None: + self.new_password = new_password + + @property + def email(self): + """Gets the email of this ChangePasswordRequest. # noqa: E501 + + + :return: The email of this ChangePasswordRequest. # noqa: E501 + :rtype: str + """ + return self._email + + @email.setter + def email(self, email): + """Sets the email of this ChangePasswordRequest. + + + :param email: The email of this ChangePasswordRequest. # noqa: E501 + :type: str + """ + + self._email = email + + @property + def old_password(self): + """Gets the old_password of this ChangePasswordRequest. # noqa: E501 + + + :return: The old_password of this ChangePasswordRequest. # noqa: E501 + :rtype: str + """ + return self._old_password + + @old_password.setter + def old_password(self, old_password): + """Sets the old_password of this ChangePasswordRequest. + + + :param old_password: The old_password of this ChangePasswordRequest. # noqa: E501 + :type: str + """ + + self._old_password = old_password + + @property + def new_password(self): + """Gets the new_password of this ChangePasswordRequest. # noqa: E501 + + + :return: The new_password of this ChangePasswordRequest. # noqa: E501 + :rtype: str + """ + return self._new_password + + @new_password.setter + def new_password(self, new_password): + """Sets the new_password of this ChangePasswordRequest. + + + :param new_password: The new_password of this ChangePasswordRequest. # noqa: E501 + :type: str + """ + + self._new_password = new_password + + def to_dict(self): + """Returns the model properties as a dict""" + result = {} + + for attr, _ in six.iteritems(self.swagger_types): + value = getattr(self, attr) + if isinstance(value, list): + result[attr] = list(map( + lambda x: x.to_dict() if hasattr(x, "to_dict") else x, + value + )) + elif hasattr(value, "to_dict"): + result[attr] = value.to_dict() + elif isinstance(value, dict): + result[attr] = dict(map( + lambda item: (item[0], item[1].to_dict()) + if hasattr(item[1], "to_dict") else item, + value.items() + )) + else: + result[attr] = value + if issubclass(ChangePasswordRequest, dict): + for key, value in self.items(): + result[key] = value + + return result + + def to_str(self): + """Returns the string representation of the model""" + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + if not isinstance(other, ChangePasswordRequest): + return False + + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + """Returns true if both objects are not equal""" + return not self == other diff --git a/specs/zrok.yml b/specs/zrok.yml index 29a596a0b..74b522bb7 100644 --- a/specs/zrok.yml +++ b/specs/zrok.yml @@ -15,6 +15,32 @@ paths: # # account # + /changePassword: + post: + tags: + - account + security: + - key: [] + operationId: changePassword + parameters: + - name: body + in: body + schema: + $ref: '#/definitions/changePasswordRequest' + responses: + 200: + description: changed password + 400: + description: password not changed + 401: + description: unauthorized + 422: + description: password validation failure + schema: + $ref: '#/definitions/errorMessage' + 500: + description: internal server error + /invite: post: tags: @@ -703,6 +729,16 @@ definitions: password: type: string + changePasswordRequest: + type: object + properties: + email: + type: string + oldPassword: + type: string + newPassword: + type: string + configuration: type: object properties: diff --git a/ui/src/api/account.js b/ui/src/api/account.js index 545dd7fbc..81447ad0c 100644 --- a/ui/src/api/account.js +++ b/ui/src/api/account.js @@ -2,6 +2,21 @@ // Auto-generated, edits will be overwritten import * as gateway from './gateway' +/** + * @param {object} options Optional options + * @param {module:types.changePasswordRequest} [options.body] + * @return {Promise} changed password + */ +export function changePassword(options) { + if (!options) options = {} + const parameters = { + body: { + body: options.body + } + } + return gateway.request(changePasswordOperation, parameters) +} + /** * @param {object} options Optional options * @param {module:types.inviteRequest} [options.body] @@ -107,6 +122,17 @@ export function verify(options) { return gateway.request(verifyOperation, parameters) } +const changePasswordOperation = { + path: '/changePassword', + contentTypes: ['application/zrok.v1+json'], + method: 'post', + security: [ + { + id: 'key' + } + ] +} + const inviteOperation = { path: '/invite', contentTypes: ['application/zrok.v1+json'], diff --git a/ui/src/api/types.js b/ui/src/api/types.js index 946549262..bcadc5f9e 100644 --- a/ui/src/api/types.js +++ b/ui/src/api/types.js @@ -25,6 +25,15 @@ * @property {string} password */ +/** + * @typedef changePasswordRequest + * @memberof module:types + * + * @property {string} email + * @property {string} oldPassword + * @property {string} newPassword + */ + /** * @typedef configuration * @memberof module:types diff --git a/ui/src/components/password.js b/ui/src/components/password.js index 5e83d2513..2051433ef 100644 --- a/ui/src/components/password.js +++ b/ui/src/components/password.js @@ -45,28 +45,28 @@ const PasswordForm = (props) => { { (props.passwordLength > 0 || props.passwordRequireCapital || props.passwordRequireNumeric || props.passwordRequireSpecial) && -

Password Requirements

+

Password Requirements

} { props.passwordLength > 0 && - Minimum Length of {props.passwordLength} + Minimum Length of {props.passwordLength} } { props.passwordRequireCapital && - Requires at least 1 Capital Letter + Requires at least 1 Capital Letter } { props.passwordRequireNumeric && - Requires at least 1 Digit + Requires at least 1 Digit } { props.passwordRequireSpecial && - Requires at least 1 Special Character - {props.passwordValidSpecialCharacters.split("").join(" ")} + Requires at least 1 Special Character + {props.passwordValidSpecialCharacters.split("").join(" ")} } - + { - const [showResetTokenModal, setShowResetTokenModal] = useState(false); - const openResetTokenModal = () => setShowResetTokenModal(true); - const closeResetTokenModal = () => setShowResetTokenModal(false); + const [showRegenerateTokenModal, setShowRegenerateTokenModal] = useState(false); + const openRegenerateTokenModal = () => setShowRegenerateTokenModal(true); + const closeRegenerateTokenModal = () => setShowRegenerateTokenModal(false); + + const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); + const openChangePasswordModal = () => setShowChangePasswordModal(true); + const closeChangePasswordModal = () => setShowChangePasswordModal(false); return (
-

Regenerate your account token (DANGER!)?

-

- Regenerating your account token will stop all environments and shares from operating properly! -

-

- You will need to manually edit your - ${HOME}/.zrok/environment.json files (in each environment) to use the new - zrok_token . Updating these files will restore the functionality of your environments. -

-

- Alternatively, you can just zrok disable any enabled environments and re-enable using the - new account token. Running zrok disable will delete your environments and - any shares they contain (including reserved shares). So if you have environments and reserved shares you - need to preserve, your best bet is to update the zrok_token in those environments as - described above. -

- - -
+
+

Change Password?

+

Change your password here. Note that this will not log you out of any already logged in sessions.

+ + +
+ +
+ +
+

Regenerate your account token (DANGER!)?

+

+ Regenerating your account token will stop all environments and shares from operating properly! +

+

+ You will need to manually edit your + ${HOME}/.zrok/environment.json files (in each environment) to use the new + zrok_token . Updating these files will restore the functionality of your environments. +

+

+ Alternatively, you can just zrok disable any enabled environments and re-enable using the + new account token. Running zrok disable will delete your environments and + any shares they contain (including reserved shares). So if you have environments and reserved shares you + need to preserve, your best bet is to update the zrok_token in those environments as + described above. +

+ + +
+ ) } diff --git a/ui/src/console/detail/account/actions/ChangePassword.js b/ui/src/console/detail/account/actions/ChangePassword.js new file mode 100644 index 000000000..d0dab9911 --- /dev/null +++ b/ui/src/console/detail/account/actions/ChangePassword.js @@ -0,0 +1,142 @@ +import React, {useEffect, useState} from "react"; +import * as account from "../../../../api/account"; +import {Button, Container, Form, Row} from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import * as metadata from "../../../../api/metadata"; + +const validatePassword = (password, l, rc, rn, rs, spc, cb) => { + if(password.length < l) { + cb(false, "Entered password is too short! (" + l + " characters minimum)!"); + return; + } + if(rc && !/[A-Z]/.test(password)) { + cb(false, "Entered password requires a capital letter!"); + return; + } + if(rn && !/\d/.test(password)) { + cb(false, "Entered password requires a digit!"); + return; + } + if(rs) { + if(!spc.split("").some(v => password.includes(v))) { + cb(false, "Entered password requires a special character!"); + return; + } + } + return cb(true, ""); +} + +const ChangePassword = (props) => { + const [oldPassword, setOldPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [message, setMessage] = useState(''); + + const [passwordLength, setPasswordLength] = useState(8); + const [passwordRequireCapital, setPasswordRequireCapital] = useState(true); + const [passwordRequireNumeric, setPasswordRequireNumeric] = useState(true); + const [passwordRequireSpecial, setPasswordRequireSpecial] = useState(true); + const [passwordValidSpecialCharacters, setPasswordValidSpecialCharacters] = useState(""); + + useEffect(() => { + metadata.configuration().then(resp => { + if (!resp.error) { + setPasswordLength(resp.data.passwordRequirements.length) + setPasswordRequireCapital(resp.data.passwordRequirements.requireCapital) + setPasswordRequireNumeric(resp.data.passwordRequirements.requireNumeric) + setPasswordRequireSpecial(resp.data.passwordRequirements.requireSpecial) + setPasswordValidSpecialCharacters(resp.data.passwordRequirements.validSpecialCharacters) + } + }).catch(err => { + console.log("error getting configuration", err); + }); + }, []) + + const handleSubmit = async e => { + e.preventDefault(); + + let ok = false; + validatePassword(newPassword, + passwordLength, + passwordRequireCapital, + passwordRequireNumeric, + passwordRequireSpecial, + passwordValidSpecialCharacters, (isOk, msg) => { ok = isOk; setMessage(msg); }) + if(!ok) { + return; + } + + if(confirmPassword !== newPassword) { + setMessage("New password and confirmation do not match!"); + return; + } + + account.changePassword({ body: { oldPassword: oldPassword, newPassword: newPassword, email: props.user.email } }) + .then(resp => { + if (!resp.error) { + console.log("resp", resp) + setMessage("Password successfully changed!"); + } else { + setMessage("Failure changing password! Is old password correct?"); + } + }).catch(resp => { + console.log("resp", resp) + setMessage("Failure changing password! Is old password correct?") + }) + + } + + let hide = () => { + props.onHide(); + setMessage(""); + setOldPassword(""); + setNewPassword(""); + setConfirmPassword(""); + } + + return ( + + Change Password + +
+ + + { setMessage(''); setOldPassword(t.target.value); }} + value={oldPassword} + style={{ marginBottom: "1em" }} + /> + + + { setMessage(''); setNewPassword(t.target.value); }} + value={newPassword} + style={{ marginBottom: "1em" }} + /> + + + { setMessage(''); setConfirmPassword(t.target.value); }} + value={confirmPassword} + /> + + +

{message}

+
+ + + +
+
+
+
+ ); +} + +export default ChangePassword; \ No newline at end of file diff --git a/ui/src/console/detail/account/actions/RegenerateToken.js b/ui/src/console/detail/account/actions/RegenerateToken.js new file mode 100644 index 000000000..e74af0c69 --- /dev/null +++ b/ui/src/console/detail/account/actions/RegenerateToken.js @@ -0,0 +1,82 @@ +import Modal from "react-bootstrap/Modal"; +import {Button, Container, Form, Row} from "react-bootstrap"; +import React, {useState} from "react"; +import * as account from "../../../../api/account"; + +const RegenerateToken = (props) => { + const [confirmEmail, setConfirmEmail] = useState(''); + const [message, setMessage] = useState(''); + + const hide = () => { + props.onHide(); + setConfirmEmail(''); + setMessage(''); + }; + + const handleSubmit = (event) => { + event.preventDefault(); + + if(confirmEmail !== props.user.email) { + setMessage("Email address confirmation does not match!"); + return; + } + + account.resetToken({body: {emailAddress: props.user.email}}) + .then(resp => { + console.log(resp); + let user = JSON.parse(localStorage.getItem('user')); + localStorage.setItem('user', JSON.stringify({ + email: user.email, + token: resp.data.token + })); + document.dispatchEvent(new Event('storage')); + setMessage("Your new account token is: " + resp.data.token); + }).catch(err => { + setMessage("Account token regeneration failed!"); + console.log("account token regeneration failed", err); + }); + }; + + return ( + + Are you very sure? + +
+ +

+ Did you read the warning on the previous screen? This action will reset all of your active + environments and shares! +

+

+ You will need to update each of + your ${HOME}/.zrok/environments.yml files + with your new token to allow them to continue working! +

+

+ Hit Escape or click the 'X' to abort! +

+ + { + setMessage(''); + setConfirmEmail(t.target.value); + }} + value={confirmEmail} + style={{marginBottom: "1em"}} + /> + + +

{message}

+
+ + + +
+
+
+
+ ); +}; + +export default RegenerateToken; \ No newline at end of file diff --git a/ui/src/console/detail/account/actions/ResetToken.js b/ui/src/console/detail/account/actions/ResetToken.js deleted file mode 100644 index 8a9b266af..000000000 --- a/ui/src/console/detail/account/actions/ResetToken.js +++ /dev/null @@ -1,95 +0,0 @@ -import React, {useRef, useState} from "react"; -import Modal from "react-bootstrap/Modal"; -import {mdiContentCopy} from "@mdi/js"; -import Icon from "@mdi/react"; -import { Button, Overlay, Tooltip } from "react-bootstrap"; -import * as account from "../../../../api/account"; - -const ResetToken = (props) => { - const target = useRef(null); - const [showTooltip, setShowTooltip] = useState(false); - - const handleCopy = async () => { - let copiedText = document.getElementById("zrok-token").innerHTML; - try { - await navigator.clipboard.writeText(copiedText); - - setShowTooltip(true); - setTimeout(() => setShowTooltip(false), 1000); - - } catch(err) { - console.error("failed to copy", err); - } - } - - let resetToken = () => { - account.resetToken({ body: { "emailAddress": props.user.email } }).then(resp => { - console.log(resp) - let user = JSON.parse(localStorage.getItem('user')) - localStorage.setItem('user', JSON.stringify({ - "email": user.email, - "token": resp.data.token - })); - document.dispatchEvent(new Event('storage')) - setModalBody(( -
-

- You will need to update your environment files ${HOME}/.zrok/environment.json - with the new zrok_token . -

-

- Your new zrok_token is: {resp.data.token}{' '} - -

- -
- )); - setModalHeader(( - Account Token Regenerated! - )) - }).catch(err => { - console.log("err", err); - }); - } - - let hide = () => { - setModalHeader(defaultHeader) - setModalBody(defaultModal) - props.onHide() - } - - let defaultHeader = (Are you sure?) - let defaultModal = ( -
-

Did you read the warning on the previous screen? This action will reset all of your active environments and shares!

-

You will need to update each of your ${HOME}/.zrok/environments.yml files with your new token!

-

- - -

-
- ); - - const [modalBody, setModalBody] = useState(defaultModal); - const [modalHeader, setModalHeader] = useState(defaultHeader); - - return ( -
- - {modalHeader} - - {modalBody} - - - - {(props) => ( - - Copied! - - )} - -
- ) -} - -export default ResetToken; \ No newline at end of file