-
Notifications
You must be signed in to change notification settings - Fork 107
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add changing user password route for htpasswd
Signed-off-by: onidoru <[email protected]>
- Loading branch information
Showing
12 changed files
with
775 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
package api | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"os" | ||
"strings" | ||
"sync" | ||
|
||
"golang.org/x/crypto/bcrypt" | ||
|
||
zerr "zotregistry.io/zot/errors" | ||
"zotregistry.io/zot/pkg/storage/constants" | ||
) | ||
|
||
const ( | ||
htpasswdValidTokensNumber = 2 | ||
) | ||
|
||
type HtpasswdClient struct { | ||
credMap credMap | ||
filepath string | ||
} | ||
|
||
type credMap struct { | ||
m map[string]string | ||
rw *sync.RWMutex | ||
} | ||
|
||
func NewHtpasswdClient(filepath string) *HtpasswdClient { | ||
return &HtpasswdClient{ | ||
filepath: filepath, | ||
credMap: credMap{ | ||
m: make(map[string]string), | ||
rw: &sync.RWMutex{}, | ||
}, | ||
} | ||
} | ||
|
||
// Init initializes the HtpasswdClient. | ||
// It performs the file read using the filename specified in NewHtpasswdClient | ||
// and caches all user passwords. | ||
func (hc *HtpasswdClient) Init() error { | ||
credsFile, err := os.Open(hc.filepath) | ||
if err != nil { | ||
return fmt.Errorf("error occurred while opening creds-file: %w", err) | ||
} | ||
defer credsFile.Close() | ||
|
||
hc.credMap.rw.Lock() | ||
defer hc.credMap.rw.Unlock() | ||
|
||
scanner := bufio.NewScanner(credsFile) | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
if strings.Contains(line, ":") { | ||
tokens := strings.Split(line, ":") | ||
if len(tokens) == htpasswdValidTokensNumber { | ||
hc.credMap.m[tokens[0]] = tokens[1] | ||
} | ||
} | ||
} | ||
|
||
if err := scanner.Err(); err != nil { | ||
return fmt.Errorf("error occurred while reading creds-file: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Get returns the password associated with the login and a bool | ||
// indicating whether the login was found. | ||
// It does not check whether the user's password is correct. | ||
func (hc *HtpasswdClient) Get(login string) (string, bool) { | ||
return hc.credMap.Get(login) | ||
} | ||
|
||
// Set sets the new password. It does not perform any checks, | ||
// the only error is possible is encryption error. | ||
func (hc *HtpasswdClient) Set(login, password string) error { | ||
passphrase, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||
if err != nil { | ||
return fmt.Errorf("error occurred while cheking passwords: %w", err) | ||
} | ||
|
||
return hc.credMap.Set(login, string(passphrase)) | ||
} | ||
|
||
// CheckPassword checks whether the user has a specified password. | ||
// It returns an error if the user is not found or passwords do not match, | ||
// and returns the nil on passwords match. | ||
func (hc *HtpasswdClient) CheckPassword(login, password string) error { | ||
passwordHash, ok := hc.Get(login) | ||
if !ok { | ||
return zerr.ErrUserIsNotFound | ||
} | ||
|
||
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)) | ||
if err != nil { | ||
return zerr.ErrPasswordsDoNotMatch | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// ChangePassword changes the user password. | ||
// It accepts user login, his supposed old password for verification and new password. | ||
func (hc *HtpasswdClient) ChangePassword(login, supposedOldPassword, newPassword string) error { | ||
if len(newPassword) == 0 { | ||
return zerr.ErrPasswordIsEmpty | ||
} | ||
|
||
hc.credMap.rw.RLock() | ||
oldPassphrase, ok := hc.credMap.m[login] | ||
hc.credMap.rw.RUnlock() | ||
|
||
if !ok { | ||
return zerr.ErrUserIsNotFound | ||
} | ||
|
||
// given old password must match actual old password | ||
if err := bcrypt.CompareHashAndPassword([]byte(oldPassphrase), []byte(supposedOldPassword)); err != nil { | ||
return zerr.ErrOldPasswordIsWrong | ||
} | ||
|
||
// if passwords match, no need to update file and map, return nil as if operation is successful | ||
if err := bcrypt.CompareHashAndPassword([]byte(oldPassphrase), []byte(newPassword)); err == nil { | ||
return nil | ||
} | ||
|
||
// encrypt new password | ||
newPassphrase, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) | ||
if err != nil { | ||
return fmt.Errorf("error occurred while encrypting new password: %w", err) | ||
} | ||
|
||
file, err := os.ReadFile(hc.filepath) | ||
if err != nil { | ||
return fmt.Errorf("error occurred while reading creds-file: %w", err) | ||
} | ||
|
||
// read passwords line by line to find the corresponding login | ||
lines := strings.Split(string(file), "\n") | ||
for i, line := range lines { | ||
if tokens := strings.Split(line, ":"); len(tokens) == htpasswdValidTokensNumber { | ||
if tokens[0] == login { | ||
lines[i] = tokens[0] + ":" + string(newPassphrase) | ||
|
||
break | ||
} | ||
} | ||
} | ||
|
||
// write new content to file | ||
output := strings.Join(lines, "\n") | ||
|
||
err = os.WriteFile(hc.filepath, []byte(output), constants.DefaultDirPerms) | ||
if err != nil { | ||
return fmt.Errorf("error occurred while writing to creds-file: %w", err) | ||
} | ||
|
||
// set to credMap only if all file operations are successful to prevent collisions | ||
hc.credMap.rw.Lock() | ||
hc.credMap.m[login] = string(newPassphrase) | ||
hc.credMap.rw.Unlock() | ||
|
||
return nil | ||
} | ||
|
||
func (c credMap) Set(login, passphrase string) error { | ||
c.rw.Lock() | ||
c.m[login] = passphrase | ||
c.rw.Unlock() | ||
|
||
return nil | ||
} | ||
|
||
func (c credMap) Get(login string) (string, bool) { | ||
c.rw.RLock() | ||
defer c.rw.RUnlock() | ||
passphrase, ok := c.m[login] | ||
|
||
return passphrase, ok | ||
} |
Oops, something went wrong.