| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- /*
- Copyright © The ESO 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
- https://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 passworddepot
- import (
- "bytes"
- "context"
- "crypto/tls"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "strings"
- "time"
- )
- const (
- // DoRequestError is the error format string for request errors.
- DoRequestError = "error: do request: %w"
- )
- // HTTPClient is an interface representing the ability to perform HTTP requests.
- type HTTPClient interface {
- Do(*http.Request) (*http.Response, error)
- }
- // AccessData represents the access credentials returned by the Password Depot API upon successful login.
- type AccessData struct {
- ClientID string `json:"client_id"`
- AccessToken string `json:"access_token"`
- }
- // Databases represents the list of Password Depot databases accessible with the current credentials.
- type Databases struct {
- Databases []struct {
- Name string `json:"name"`
- Fingerprint string `json:"fingerprint"`
- Date time.Time `json:"date"`
- Rights string `json:"rights"`
- Reasondelete string `json:"reasondelete"`
- } `json:"databases"`
- Infoclasses string `json:"infoclasses"`
- Policyforce string `json:"policyforce"`
- Policyminlength string `json:"policyminlength"`
- Policyincludeatleast string `json:"policyincludeatleast"`
- Policymingroups string `json:"policymingroups"`
- Policyselectedgroups string `json:"policyselectedgroups"`
- }
- // DatabaseEntries represents the entries in a Password Depot database.
- type DatabaseEntries struct {
- Name string `json:"name"`
- Parent string `json:"parent"`
- Entries []Entry `json:"entries"`
- Infoclasses string `json:"infoclasses"`
- Reasondelete string `json:"reasondelete"`
- }
- // Entry represents a single entry in the Password Depot database.
- type Entry struct {
- Name string `json:"name"`
- Login string `json:"login"`
- Password string `json:"pass"`
- URL string `json:"url"`
- Importance string `json:"importance"`
- Date time.Time `json:"date"`
- Icon string `json:"icon"`
- Secondeye string `json:"secondeye"`
- Fingerprint string `json:"fingerprint"`
- Rights string `json:"rights"`
- Itemclass string `json:"itemclass"`
- }
- // API represents a client for the Password Depot API.
- type API struct {
- client HTTPClient
- baseURL string
- hostPort string
- secret *AccessData
- password string
- username string
- }
- // SecretEntry represents a secret entry in Password Depot.
- type SecretEntry struct {
- Name string `json:"name"`
- Fingerprint string `json:"fingerprint"`
- Itemclass string `json:"itemclass"`
- Login string `json:"login"`
- Pass string `json:"pass"`
- URL string `json:"url"`
- Importance string `json:"importance"`
- Date time.Time `json:"date"`
- Comment string `json:"comment"`
- Expirydate string `json:"expirydate"`
- Tags string `json:"tags"`
- Author string `json:"author"`
- Category string `json:"category"`
- Icon string `json:"icon"`
- Secondeye string `json:"secondeye"`
- Secondpass string `json:"secondpass"`
- Template string `json:"template"`
- Acm string `json:"acm"`
- Paramstr string `json:"paramstr"`
- Loginid string `json:"loginid"`
- Passid string `json:"passid"`
- Donotaddon string `json:"donotaddon"`
- Markassafe string `json:"markassafe"`
- Safemode string `json:"safemode"`
- }
- var errDBNotFound = errors.New("database not found")
- var errSecretNotFound = errors.New("secret not found")
- // NewAPI creates a new instance of the PasswordDepot API client and performs login.
- func NewAPI(ctx context.Context, baseURL, username, password, hostPort string) (*API, error) {
- api := &API{
- baseURL: baseURL,
- hostPort: hostPort,
- username: username,
- password: password,
- }
- tr := &http.Transport{
- TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
- }
- api.client = &http.Client{Transport: tr}
- err := api.login(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to login: %w", err)
- }
- return api, nil
- }
- func (api *API) doAuthenticatedRequest(r *http.Request) (*http.Response, error) {
- r.Header.Add("access_token", api.secret.AccessToken)
- r.Header.Add("client_id", api.secret.ClientID)
- return api.client.Do(r)
- }
- func (api *API) getDatabaseFingerprint(database string) (string, error) {
- databases, err := api.ListDatabases()
- if err != nil {
- return "", fmt.Errorf("error: getting database list: %w", err)
- }
- for _, db := range databases.Databases {
- if strings.Contains(db.Name, database) {
- return db.Fingerprint, nil
- }
- }
- return "", errDBNotFound
- }
- func (api *API) getSecretFingerprint(databaseFingerprint, secretName, folder string) (string, error) {
- secrets, err := api.ListSecrets(databaseFingerprint, folder)
- if err != nil {
- return "", fmt.Errorf("error: getting secrets list: %w", err)
- }
- parts := strings.Split(secretName, ".")
- searchName := parts[0]
- var fingerprint string
- for _, entry := range secrets.Entries {
- if strings.Contains(entry.Name, searchName) {
- fingerprint = entry.Fingerprint
- if len(parts) > 1 {
- return api.getSecretFingerprint(databaseFingerprint, strings.Join(parts[1:], "."), fingerprint)
- }
- return fingerprint, nil
- }
- }
- return "", errSecretNotFound
- }
- func (api *API) getendpointURL(endpoint string) string {
- return fmt.Sprintf("https://%s:%s/v1.0/%s", api.baseURL, api.hostPort, endpoint)
- }
- func (api *API) login(ctx context.Context) error {
- loginRequest, err := http.NewRequestWithContext(ctx, "GET", api.getendpointURL("login"), http.NoBody)
- if err != nil {
- return fmt.Errorf("error creating request: %w", err)
- }
- loginRequest.Header.Add("user", api.username)
- loginRequest.Header.Add("pass", api.password)
- resp, err := api.client.Do(loginRequest) //nolint:bodyclose // linters bug
- if err != nil {
- return fmt.Errorf(DoRequestError, err)
- }
- accessData := AccessData{}
- err = ReadAndUnmarshal(resp, &accessData)
- if err != nil {
- return fmt.Errorf("error: failed to unmarshal response body: %w", err)
- }
- api.secret = &accessData
- return nil
- }
- // ListSecrets retrieves the list of secrets from the specified database and folder.
- func (api *API) ListSecrets(dbFingerprint, folder string) (DatabaseEntries, error) {
- endpointURL := api.getendpointURL(fmt.Sprintf("list?db=%s", dbFingerprint))
- if folder != "" {
- endpointURL = fmt.Sprintf("%s&folder=%s", endpointURL, folder)
- }
- listSecrets, err := http.NewRequest("GET", endpointURL, http.NoBody)
- if err != nil {
- return DatabaseEntries{}, fmt.Errorf("error: creating secrets request: %w", err)
- }
- respSecretsList, err := api.doAuthenticatedRequest(listSecrets) //nolint:bodyclose // linters bug
- if err != nil {
- return DatabaseEntries{}, fmt.Errorf(DoRequestError, err)
- }
- dbEntries := DatabaseEntries{}
- err = ReadAndUnmarshal(respSecretsList, &dbEntries)
- return dbEntries, err
- }
- // ReadAndUnmarshal reads the response body and unmarshals it into the target struct.
- func ReadAndUnmarshal(resp *http.Response, target any) error {
- var buf bytes.Buffer
- defer func() {
- _ = resp.Body.Close()
- }()
- if resp.StatusCode < 200 || resp.StatusCode > 299 {
- return fmt.Errorf("failed to authenticate with the given credentials: %d %s", resp.StatusCode, buf.String())
- }
- _, err := buf.ReadFrom(resp.Body)
- if err != nil {
- return err
- }
- return json.Unmarshal(buf.Bytes(), target)
- }
- // ListDatabases retrieves the list of databases accessible with the current credentials.
- func (api *API) ListDatabases() (Databases, error) {
- listDBRequest, err := http.NewRequest("GET", api.getendpointURL("list"), http.NoBody)
- if err != nil {
- return Databases{}, fmt.Errorf("error: creating db request: %w", err)
- }
- respDBList, err := api.doAuthenticatedRequest(listDBRequest) //nolint:bodyclose // linters bug
- if err != nil {
- return Databases{}, fmt.Errorf(DoRequestError, err)
- }
- databases := Databases{}
- err = ReadAndUnmarshal(respDBList, &databases)
- return databases, err
- }
- // GetSecret retrieves a secret by its name from the specified database.
- func (api *API) GetSecret(database, secretName string) (SecretEntry, error) {
- dbFingerprint, err := api.getDatabaseFingerprint(database)
- if err != nil {
- return SecretEntry{}, fmt.Errorf("error: getting DB fingerprint: %w", err)
- }
- secretFingerprint, err := api.getSecretFingerprint(dbFingerprint, secretName, "")
- if err != nil {
- return SecretEntry{}, fmt.Errorf("error: getting Secret fingerprint: %w", err)
- }
- readSecretRequest, err := http.NewRequest("GET", api.getendpointURL(fmt.Sprintf("read?db=%s&entry=%s", dbFingerprint, secretFingerprint)), http.NoBody)
- if err != nil {
- return SecretEntry{}, fmt.Errorf("error: creating secrets request: %w", err)
- }
- respSecretRead, err := api.doAuthenticatedRequest(readSecretRequest) //nolint:bodyclose // linters bug
- if err != nil {
- return SecretEntry{}, fmt.Errorf(DoRequestError, err)
- }
- secretEntry := SecretEntry{}
- err = ReadAndUnmarshal(respSecretRead, &secretEntry)
- return secretEntry, err
- }
- // ToMap converts the SecretEntry struct to a map[string][]byte.
- func (s *SecretEntry) ToMap() map[string][]byte {
- m := make(map[string][]byte)
- m["name"] = []byte(s.Name)
- m["fingerprint"] = []byte(s.Fingerprint)
- m["itemclass"] = []byte(s.Itemclass)
- m["login"] = []byte(s.Login)
- m["pass"] = []byte(s.Pass)
- m["url"] = []byte(s.URL)
- m["importance"] = []byte(s.Importance)
- m["comment"] = []byte(s.Comment)
- m["expirydate"] = []byte(s.Expirydate)
- m["tags"] = []byte(s.Tags)
- m["author"] = []byte(s.Author)
- m["category"] = []byte(s.Category)
- m["icon"] = []byte(s.Icon)
- m["secondeye"] = []byte(s.Secondeye)
- m["secondpass"] = []byte(s.Secondpass)
- m["template"] = []byte(s.Template)
- m["acm"] = []byte(s.Acm)
- m["paramstr"] = []byte(s.Paramstr)
- m["loginid"] = []byte(s.Loginid)
- m["passid"] = []byte(s.Passid)
- m["donotaddon"] = []byte(s.Donotaddon)
- m["markassafe"] = []byte(s.Markassafe)
- m["safemode"] = []byte(s.Safemode)
- return m
- }
|