From ff29c83d56f75b64fb6d6869c66f3221d2b88324 Mon Sep 17 00:00:00 2001 From: Samuel Lorch Date: Mon, 30 Aug 2021 13:52:31 +0200 Subject: [PATCH] Move code from Internal Git --- api.go | 57 ++++++++++++ auth.go | 206 +++++++++++++++++++++++++++++++++++++++++ client.go | 152 ++++++++++++++++++++++++++++++ comments.go | 80 ++++++++++++++++ encryption.go | 20 ++++ errors.go | 8 ++ favorites.go | 39 ++++++++ folders.go | 117 ++++++++++++++++++++++++ go.mod | 13 +++ go.sum | 71 +++++++++++++++ gpgkey.go | 57 ++++++++++++ groups.go | 97 ++++++++++++++++++++ healthcheck.go | 26 ++++++ helper/folder.go | 33 +++++++ helper/resources.go | 208 ++++++++++++++++++++++++++++++++++++++++++ helper/share.go | 218 ++++++++++++++++++++++++++++++++++++++++++++ misc.go | 25 +++++ permissions.go | 50 ++++++++++ resource_types.go | 49 ++++++++++ resources.go | 135 +++++++++++++++++++++++++++ roles.go | 55 +++++++++++ secrets.go | 36 ++++++++ share.go | 93 +++++++++++++++++++ staticcheck.conf | 10 ++ time.go | 29 ++++++ users.go | 124 +++++++++++++++++++++++++ 26 files changed, 2008 insertions(+) create mode 100644 api.go create mode 100644 auth.go create mode 100644 client.go create mode 100644 comments.go create mode 100644 encryption.go create mode 100644 errors.go create mode 100644 favorites.go create mode 100644 folders.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gpgkey.go create mode 100644 groups.go create mode 100644 healthcheck.go create mode 100644 helper/folder.go create mode 100644 helper/resources.go create mode 100644 helper/share.go create mode 100644 misc.go create mode 100644 permissions.go create mode 100644 resource_types.go create mode 100644 resources.go create mode 100644 roles.go create mode 100644 secrets.go create mode 100644 share.go create mode 100644 staticcheck.conf create mode 100644 time.go create mode 100644 users.go diff --git a/api.go b/api.go new file mode 100644 index 0000000..b147046 --- /dev/null +++ b/api.go @@ -0,0 +1,57 @@ +package passbolt + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +// APIResponse is the Struct representation of a Json Response +type APIResponse struct { + Header APIHeader `json:"header"` + Body json.RawMessage `json:"body"` +} + +// APIHeader is the Struct representation of the Header of a APIResponse +type APIHeader struct { + ID string `json:"id"` + Status string `json:"status"` + Servertime int `json:"servertime"` + Action string `json:"action"` + Message string `json:"message"` + URL string `json:"url"` + Code int `json:"code"` +} + +// DoCustomRequest Executes a Custom Request and returns a APIResponse +func (c *Client) DoCustomRequest(ctx context.Context, method, path, version string, body interface{}, opts interface{}) (*APIResponse, error) { + _, response, err := c.DoCustomRequestAndReturnRawResponse(ctx, method, path, version, body, opts) + return response, err +} + +func (c *Client) DoCustomRequestAndReturnRawResponse(ctx context.Context, method, path, version string, body interface{}, opts interface{}) (*http.Response, *APIResponse, error) { + u, err := addOptions(path, version, opts) + if err != nil { + return nil, nil, fmt.Errorf("Adding Request Options: %w", err) + } + + req, err := c.newRequest(method, u, body) + if err != nil { + return nil, nil, fmt.Errorf("Creating New Request: %w", err) + } + + var res APIResponse + r, err := c.do(ctx, req, &res) + if err != nil { + return r, &res, fmt.Errorf("Doing Request: %w", err) + } + + if res.Header.Status == "success" { + return r, &res, nil + } else if res.Header.Status == "error" { + return r, &res, fmt.Errorf("%w: Message: %v, Body: %v", ErrAPIResponseErrorStatusCode, res.Header.Message, string(res.Body)) + } else { + return r, &res, fmt.Errorf("%w: Message: %v, Body: %v", ErrAPIResponseUnknownStatusCode, res.Header.Message, string(res.Body)) + } +} diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..3133e1b --- /dev/null +++ b/auth.go @@ -0,0 +1,206 @@ +package passbolt + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/gopenpgp/v2/helper" +) + +// PublicKeyReponse the Body of a Public Key Api Request +type PublicKeyReponse struct { + Fingerprint string `json:"fingerprint"` + Keydata string `json:"keydata"` +} + +// Login is used for login +type Login struct { + Auth *GPGAuth `json:"gpg_auth"` +} + +// GPGAuth is used for login +type GPGAuth struct { + KeyID string `json:"keyid"` + Token string `json:"user_token_result,omitempty"` +} + +// TODO add Server Verification Function + +// GetPublicKey gets the Public Key and Fingerprint of the Passbolt instance +func (c *Client) GetPublicKey(ctx context.Context) (string, string, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "auth/verify.json", "v2", nil, nil) + if err != nil { + return "", "", fmt.Errorf("Doing Request: %w", err) + } + + var body PublicKeyReponse + err = json.Unmarshal(msg.Body, &body) + if err != nil { + return "", "", fmt.Errorf("Parsing JSON: %w", err) + } + // TODO check if that Fingerpirnt is actually from the Publickey + return body.Keydata, body.Fingerprint, nil +} + +// CheckSession Check to see if you have a Valid Session +func (c *Client) CheckSession(ctx context.Context) bool { + _, err := c.DoCustomRequest(ctx, "GET", "auth/is-authenticated.json", "v2", nil, nil) + return err == nil +} + +// Login gets a Session and CSRF Token from Passbolt and Stores them in the Clients Cookie Jar +func (c *Client) Login(ctx context.Context) error { + + privateKeyObj, err := crypto.NewKeyFromArmored(c.userPrivateKey) + if err != nil { + return fmt.Errorf("Parsing User Private Key: %w", err) + } + data := Login{&GPGAuth{KeyID: privateKeyObj.GetFingerprint()}} + + res, _, err := c.DoCustomRequestAndReturnRawResponse(ctx, "POST", "/auth/login.json", "v2", data, nil) + if err != nil && !strings.Contains(err.Error(), "Error API JSON Response Status: Message: The authentication failed.") { + return fmt.Errorf("Doing Stage 1 Request: %w", err) + } + + encAuthToken := res.Header.Get("X-GPGAuth-User-Auth-Token") + + if encAuthToken == "" { + return fmt.Errorf("Got Empty X-GPGAuth-User-Auth-Token Header") + } + + c.log("Got Encrypted Auth Token: %v", encAuthToken) + + encAuthToken, err = url.QueryUnescape(encAuthToken) + if err != nil { + return fmt.Errorf("Unescaping User Auth Token: %w", err) + } + encAuthToken = strings.ReplaceAll(encAuthToken, "\\ ", " ") + + authToken, err := helper.DecryptMessageArmored(c.userPrivateKey, c.userPassword, encAuthToken) + if err != nil { + return fmt.Errorf("Decrypting User Auth Token: %w", err) + } + + c.log("Decrypted Auth Token: %v", authToken) + + err = checkAuthTokenFormat(authToken) + if err != nil { + return fmt.Errorf("Checking Auth Token Format: %w", err) + } + + data.Auth.Token = string(authToken) + + res, _, err = c.DoCustomRequestAndReturnRawResponse(ctx, "POST", "/auth/login.json", "v2", data, nil) + if err != nil { + return fmt.Errorf("Doing Stage 2 Request: %w", err) + } + + c.log("Got Cookies: %+v", res.Cookies()) + + for _, cookie := range res.Cookies() { + if cookie.Name == "passbolt_session" { + c.sessionToken = *cookie + // Session Cookie in older Passbolt Versions + } else if cookie.Name == "CAKEPHP" { + c.sessionToken = *cookie + } + } + if c.sessionToken.Name == "" { + return fmt.Errorf("Cannot Find Session Cookie!") + } + + // Do Mfa Here if ever + + // You have to get a make GET Request to get the CSRF Token which is Required for Write Operations + msg, apiMsg, err := c.DoCustomRequestAndReturnRawResponse(ctx, "GET", "/users/me.json", "v2", nil, nil) + if err != nil { + c.log("is MFA Enabled? That is not yet Supported!") + return fmt.Errorf("Getting CSRF Token: %w", err) + } + + for _, cookie := range msg.Cookies() { + if cookie.Name == "csrfToken" { + c.csrfToken = *cookie + } + } + + if c.csrfToken.Name == "" { + return fmt.Errorf("Cannot Find csrfToken Cookie!") + } + + // Get Users Own Public Key from Server + var user User + err = json.Unmarshal(apiMsg.Body, &user) + if err != nil { + return fmt.Errorf("Parsing User 'Me' JSON from API Request: %w", err) + } + + // Validate that this Publickey that the Server gave us actually Matches our Privatekey + randomString, err := randStringBytesRmndr(50) + if err != nil { + return fmt.Errorf("Generating Random String as PublicKey Validation Message: %w", err) + } + armor, err := helper.EncryptMessageArmored(user.GPGKey.ArmoredKey, randomString) + if err != nil { + return fmt.Errorf("Encryping PublicKey Validation Message: %w", err) + } + decrypted, err := helper.DecryptMessageArmored(c.userPrivateKey, c.userPassword, armor) + if err != nil { + return fmt.Errorf("Decrypting PublicKey Validation Message (you might be getting Hacked): %w", err) + } + if decrypted != randomString { + return fmt.Errorf("Decrypted PublicKey Validation Message does not Match Original (you might be getting Hacked): %w", err) + } + + // Insert PublicKey into Client after checking it to Prevent ignored errors leading to proceding with a potentially Malicious PublicKey + c.userPublicKey = user.GPGKey.ArmoredKey + c.userID = user.ID + + return nil +} + +// Logout closes the current Session on the Passbolt server +func (c *Client) Logout(ctx context.Context) error { + _, err := c.DoCustomRequest(ctx, "GET", "/auth/logout.json", "v2", nil, nil) + if err != nil { + return fmt.Errorf("Doing Logout Request: %w", err) + } + c.sessionToken = http.Cookie{} + c.csrfToken = http.Cookie{} + return nil +} + +func (c *Client) GetUserID() string { + return c.userID +} + +func checkAuthTokenFormat(authToken string) error { + splitAuthToken := strings.Split(authToken, "|") + if len(splitAuthToken) != 4 { + return fmt.Errorf("Auth Token Has Wrong amount of Fields") + } + + if splitAuthToken[0] != splitAuthToken[3] { + return fmt.Errorf("Auth Token Version Fields Don't match") + } + + if !strings.HasPrefix(splitAuthToken[0], "gpgauth") { + return fmt.Errorf("Auth Token Version does not start with 'gpgauth'") + } + + length, err := strconv.Atoi(splitAuthToken[1]) + if err != nil { + return fmt.Errorf("Cannot Convert Auth Token Length Field to int: %w", err) + } + + if len(splitAuthToken[2]) != length { + return fmt.Errorf("Auth Token Data Length does not Match Length Field") + } + return nil +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..50e96c2 --- /dev/null +++ b/client.go @@ -0,0 +1,152 @@ +package passbolt + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/google/go-querystring/query" +) + +// Client is a Client struct for the Passbolt api +type Client struct { + baseURL *url.URL + userAgent string + httpClient *http.Client + + sessionToken http.Cookie + csrfToken http.Cookie + + // for some reason []byte is used for Passwords in gopenpgp instead of string like they do for keys... + userPassword []byte + userPrivateKey string + userPublicKey string + userID string + + // Enable Debug Logging + Debug bool +} + +// NewClient Returns a new Passbolt Client +func NewClient(BaseURL *url.URL, httpClient *http.Client, UserAgent, UserPrivateKey, UserPassword string) (*Client, error) { + if httpClient == nil { + httpClient = http.DefaultClient + } + if UserAgent == "" { + UserAgent = "goPassboltClient/1.0" + } + + // Verify that the Given Privatekey and Password are valid and work Together + privateKeyObj, err := crypto.NewKeyFromArmored(UserPrivateKey) + if err != nil { + return nil, fmt.Errorf("Unable to Create Key From UserPrivateKey string: %w", err) + } + unlockedKeyObj, err := privateKeyObj.Unlock([]byte(UserPassword)) + if err != nil { + return nil, fmt.Errorf("Unable to Unlock UserPrivateKey using UserPassword: %w", err) + } + privateKeyRing, err := crypto.NewKeyRing(unlockedKeyObj) + if err != nil { + return nil, fmt.Errorf("Unable to Create a new Key Ring using the unlocked UserPrivateKey: %w", err) + } + + // Cleanup Secrets + privateKeyRing.ClearPrivateParams() + + // Create Client Object + c := &Client{ + httpClient: httpClient, + baseURL: BaseURL, + userAgent: UserAgent, + userPassword: []byte(UserPassword), + userPrivateKey: UserPrivateKey, + } + return c, err +} + +func (c *Client) newRequest(method, path string, body interface{}) (*http.Request, error) { + rel, err := url.Parse(path) + if err != nil { + return nil, fmt.Errorf("Parsing URL: %w", err) + } + u := c.baseURL.ResolveReference(rel) + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, fmt.Errorf("JSON Encoding Request: %w", err) + } + } + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, fmt.Errorf("Creating HTTP Request: %w", err) + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", c.userAgent) + req.Header.Set("X-CSRF-Token", c.csrfToken.Value) + req.AddCookie(&c.sessionToken) + req.AddCookie(&c.csrfToken) + + return req, nil +} + +func (c *Client) do(ctx context.Context, req *http.Request, v *APIResponse) (*http.Response, error) { + req = req.WithContext(ctx) + resp, err := c.httpClient.Do(req) + if err != nil { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("Request Context: %w", ctx.Err()) + default: + return nil, fmt.Errorf("Request: %w", err) + } + } + defer func() { + resp.Body.Close() + }() + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return resp, fmt.Errorf("Error Reading Resopnse Body: %w", err) + } + + err = json.Unmarshal(bodyBytes, v) + if err != nil { + return resp, fmt.Errorf("Unable to Parse JSON API Response with HTTP Status Code %v: %w", resp.StatusCode, err) + } + return resp, nil +} + +func (c *Client) log(msg string, args ...interface{}) { + if !c.Debug { + return + } + fmt.Printf("[go-passbolt] "+msg+"\n", args...) +} + +func addOptions(s, version string, opt interface{}) (string, error) { + u, err := url.Parse(s) + if err != nil { + return s, fmt.Errorf("Parsing URL: %w", err) + } + + vs, err := query.Values(opt) + if err != nil { + return s, fmt.Errorf("Getting URL Query Values: %w", err) + } + if version != "" { + vs.Add("api-version", version) + } + u.RawQuery = vs.Encode() + return u.String(), nil +} diff --git a/comments.go b/comments.go new file mode 100644 index 0000000..85fac40 --- /dev/null +++ b/comments.go @@ -0,0 +1,80 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// Comment is a Comment +type Comment struct { + ID string `json:"id,omitempty"` + ParentID string `json:"parent_id,omitempty"` + ForeignKey string `json:"foreign_key,omitempty"` + Content string `json:"content,omitempty"` + ForeignModel string `json:"foreign_model,omitempty"` + Created *Time `json:"created,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + UserID string `json:"user_id,omitempty"` + Description string `json:"description,omitempty"` + Modified *Time `json:"modified,omitempty"` + ModifiedBy string `json:"modified_by,omitempty"` + Children []Comment `json:"children,omitempty"` +} + +// GetCommentsOptions are all available query parameters +type GetCommentsOptions struct { + ContainCreator bool `url:"contain[creator],omitempty"` + ContainModifier bool `url:"contain[modifier],omitempty"` +} + +// GetComments gets all Passbolt Comments an The Specified Resource +func (c *Client) GetComments(ctx context.Context, resourceID string, opts *GetCommentsOptions) ([]Comment, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/comments/resource/"+resourceID+".json", "v2", nil, opts) + if err != nil { + return nil, err + } + + var comments []Comment + err = json.Unmarshal(msg.Body, &comments) + if err != nil { + return nil, err + } + return comments, nil +} + +// CreateComment Creates a new Passbolt Comment +func (c *Client) CreateComment(ctx context.Context, resourceID string, comment Comment) (*Comment, error) { + msg, err := c.DoCustomRequest(ctx, "POST", "/comments/resource/"+resourceID+".json", "v2", comment, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &comment) + if err != nil { + return nil, err + } + return &comment, nil +} + +// UpdateComment Updates a existing Passbolt Comment +func (c *Client) UpdateComment(ctx context.Context, commentID string, comment Comment) (*Comment, error) { + msg, err := c.DoCustomRequest(ctx, "PUT", "/comments/"+commentID+".json", "v2", comment, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &comment) + if err != nil { + return nil, err + } + return &comment, nil +} + +// DeleteComment Deletes a Passbolt Comment +func (c *Client) DeleteComment(ctx context.Context, commentID string) error { + _, err := c.DoCustomRequest(ctx, "DELETE", "/comments/"+commentID+".json", "v2", nil, nil) + if err != nil { + return err + } + return nil +} diff --git a/encryption.go b/encryption.go new file mode 100644 index 0000000..146466f --- /dev/null +++ b/encryption.go @@ -0,0 +1,20 @@ +package passbolt + +import "github.com/ProtonMail/gopenpgp/v2/helper" + +// EncryptMessage encrypts a message using the users public key and then signes the message using the users private key +func (c *Client) EncryptMessage(message string) (string, error) { + return helper.EncryptSignMessageArmored(c.userPublicKey, c.userPrivateKey, c.userPassword, message) +} + +// EncryptMessageWithPublicKey encrypts a message using the provided public key and then signes the message using the users private key +func (c *Client) EncryptMessageWithPublicKey(publickey, message string) (string, error) { + return helper.EncryptSignMessageArmored(publickey, c.userPrivateKey, c.userPassword, message) +} + +// DecryptMessage decrypts a message using the users Private Key and Validates its Signature using the users public key +func (c *Client) DecryptMessage(message string) (string, error) { + // We cant Verify the signature as we don't store other users public keys locally and don't know which user did encrypt it + //return helper.DecryptVerifyMessageArmored(c.userPublicKey, c.userPrivateKey, c.userPassword, message) + return helper.DecryptMessageArmored(c.userPrivateKey, c.userPassword, message) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..d4cdc7a --- /dev/null +++ b/errors.go @@ -0,0 +1,8 @@ +package passbolt + +import "errors" + +var ( + ErrAPIResponseErrorStatusCode = errors.New("Error API JSON Response Status") + ErrAPIResponseUnknownStatusCode = errors.New("Unknown API JSON Response Status") +) diff --git a/favorites.go b/favorites.go new file mode 100644 index 0000000..646b6b5 --- /dev/null +++ b/favorites.go @@ -0,0 +1,39 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// Favorite is a Favorite +type Favorite struct { + ID string `json:"id,omitempty"` + Created *Time `json:"created,omitempty"` + ForeignKey string `json:"foreign_key,omitempty"` + ForeignModel string `json:"foreign_model,omitempty"` + Modified *Time `json:"modified,omitempty"` +} + +// CreateFavorite Creates a new Passbolt Favorite for the given Resource ID +func (c *Client) CreateFavorite(ctx context.Context, resourceID string) (*Favorite, error) { + msg, err := c.DoCustomRequest(ctx, "POST", "/favorites/resource/"+resourceID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var favorite Favorite + err = json.Unmarshal(msg.Body, &favorite) + if err != nil { + return nil, err + } + return &favorite, nil +} + +// DeleteFavorite Deletes a Passbolt Favorite +func (c *Client) DeleteFavorite(ctx context.Context, favoriteID string) error { + _, err := c.DoCustomRequest(ctx, "DELETE", "/favorites/"+favoriteID+".json", "v2", nil, nil) + if err != nil { + return err + } + return nil +} diff --git a/folders.go b/folders.go new file mode 100644 index 0000000..565e6b0 --- /dev/null +++ b/folders.go @@ -0,0 +1,117 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// Folder is a Folder +type Folder struct { + ID string `json:"id,omitempty"` + Created *Time `json:"created,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + Modified *Time `json:"modified,omitempty"` + ModifiedBy string `json:"modified_by,omitempty"` + Name string `json:"name,omitempty"` + Permissions []Permission `json:"permissions,omitempty"` + FolderParentID string `json:"folder_parent_id,omitempty"` + Personal bool `json:"personal,omitempty"` + ChildrenResources []Resource `json:"children_resources,omitempty"` + ChildrenFolders []Folder `json:"children_folders,omitempty"` +} + +// GetFolderOptions are all available query parameters +type GetFolderOptions struct { + ContainChildrenResources bool `url:"contain[children_resources],omitempty"` + ContainChildrenFolders bool `url:"contain[children_folders],omitempty"` + ContainCreator bool `url:"contain[creator],omitempty"` + ContainCreatorProfile bool `url:"contain[creator.profile],omitempty"` + ContainModifier bool `url:"contain[modifier],omitempty"` + ContainModiferProfile bool `url:"contain[modifier.profile],omitempty"` + ContainPermission bool `url:"contain[permission],omitempty"` + ContainPermissions bool `url:"contain[permissions],omitempty"` + ContainPermissionUserProfile bool `url:"contain[permissions.user.profile],omitempty"` + ContainPermissionGroup bool `url:"contain[permissions.group],omitempty"` + + FilterHasID string `url:"filter[has-id][],omitempty"` + FilterHasParent string `url:"filter[has-parent][],omitempty"` + FilterSearch string `url:"filter[search],omitempty"` +} + +// GetFolders gets all Folders from the Passboltserver +func (c *Client) GetFolders(ctx context.Context, opts *GetFolderOptions) ([]Folder, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/folders.json", "v2", nil, opts) + if err != nil { + return nil, err + } + + var body []Folder + err = json.Unmarshal(msg.Body, &body) + if err != nil { + return nil, err + } + return body, nil +} + +// CreateFolder Creates a new Passbolt Folder +func (c *Client) CreateFolder(ctx context.Context, folder Folder) (*Folder, error) { + msg, err := c.DoCustomRequest(ctx, "POST", "/folders.json", "v2", folder, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &folder) + if err != nil { + return nil, err + } + return &folder, nil +} + +// GetFolder gets a Passbolt Folder +func (c *Client) GetFolder(ctx context.Context, folderID string) (*Folder, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/folders/"+folderID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var folder Folder + err = json.Unmarshal(msg.Body, &folder) + if err != nil { + return nil, err + } + return &folder, nil +} + +// UpdateFolder Updates a existing Passbolt Folder +func (c *Client) UpdateFolder(ctx context.Context, folderID string, folder Folder) (*Folder, error) { + msg, err := c.DoCustomRequest(ctx, "PUT", "/folders/"+folderID+".json", "v2", folder, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &folder) + if err != nil { + return nil, err + } + return &folder, nil +} + +// DeleteFolder Deletes a Passbolt Folder +func (c *Client) DeleteFolder(ctx context.Context, folderID string) error { + _, err := c.DoCustomRequest(ctx, "DELETE", "/folders/"+folderID+".json", "v2", nil, nil) + if err != nil { + return err + } + return nil +} + +// MoveFolder Moves a Passbolt Folder +func (c *Client) MoveFolder(ctx context.Context, folderID, folderParentID string) error { + _, err := c.DoCustomRequest(ctx, "PUT", "/move/folder/"+folderID+".json", "v2", Folder{ + FolderParentID: folderParentID, + }, nil) + if err != nil { + return err + } + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4166a1c --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/speatzle/go-passbolt + +go 1.16 + +require ( + github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c // indirect + github.com/ProtonMail/gopenpgp/v2 v2.2.2 + github.com/google/go-querystring v1.1.0 + github.com/sirupsen/logrus v1.8.1 // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect + golang.org/x/text v0.3.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7d914dc --- /dev/null +++ b/go.sum @@ -0,0 +1,71 @@ +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= +github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c h1:FP7mMdsXy0ybzar1sJeIcZtaJka0U/ZmLTW4wRpolYk= +github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= +github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4= +github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= +github.com/ProtonMail/gopenpgp/v2 v2.2.2 h1:u2m7xt+CZWj88qK1UUNBoXeJCFJwJCZ/Ff4ymGoxEXs= +github.com/ProtonMail/gopenpgp/v2 v2.2.2/go.mod h1:ajUlBGvxMH1UBZnaYO3d1FSVzjiC6kK9XlZYGiDCvpM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/gpgkey.go b/gpgkey.go new file mode 100644 index 0000000..d7a7d00 --- /dev/null +++ b/gpgkey.go @@ -0,0 +1,57 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// GPGKey is a GPGKey +type GPGKey struct { + ID string `json:"id,omitempty"` + ArmoredKey string `json:"armored_key,omitempty"` + Created *Time `json:"created,omitempty"` + KeyCreated *Time `json:"key_created,omitempty"` + Bits int `json:"bits,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Modified *Time `json:"modified,omitempty"` + KeyID string `json:"key_id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Type string `json:"type,omitempty"` + Expires *Time `json:"expires,omitempty"` +} + +// GetGPGKeysOptions are all available query parameters +type GetGPGKeysOptions struct { + // This is a Unix TimeStamp + FilterModifiedAfter int `url:"filter[modified-after],omitempty"` +} + +// GetGPGKeys gets all Passbolt GPGKeys +func (c *Client) GetGPGKeys(ctx context.Context, opts *GetGPGKeysOptions) ([]GPGKey, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/gpgkeys.json", "v2", nil, opts) + if err != nil { + return nil, err + } + + var gpgkeys []GPGKey + err = json.Unmarshal(msg.Body, &gpgkeys) + if err != nil { + return nil, err + } + return gpgkeys, nil +} + +// GetGPGKey gets a Passbolt GPGKey +func (c *Client) GetGPGKey(ctx context.Context, gpgkeyID string) (*GPGKey, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/gpgkeys/"+gpgkeyID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var gpgkey GPGKey + err = json.Unmarshal(msg.Body, &gpgkey) + if err != nil { + return nil, err + } + return &gpgkey, nil +} diff --git a/groups.go b/groups.go new file mode 100644 index 0000000..6b13887 --- /dev/null +++ b/groups.go @@ -0,0 +1,97 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +//Group is a Group +type Group struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Created *Time `json:"created,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Modified *Time `json:"modified,omitempty"` + ModifiedBy string `json:"modified_by,omitempty"` + GroupUsers []User `json:"groups_users,omitempty"` +} + +// GetGroupsOptions are all available query parameters +type GetGroupsOptions struct { + FilterHasUsers []string `url:"filter[has_users],omitempty"` + FilterHasManagers []string `url:"filter[has-managers],omitempty"` + + ContainModifier bool `url:"contain[modifier],omitempty"` + ContainModifierProfile bool `url:"contain[modifier.profile],omitempty"` + ContainUser bool `url:"contain[user],omitempty"` + ContainGroupUser bool `url:"contain[group_user],omitempty"` + ContainMyGroupUser bool `url:"contain[my_group_user],omitempty"` +} + +// GetGroups gets all Passbolt Groups +func (c *Client) GetGroups(ctx context.Context, opts *GetGroupsOptions) ([]Group, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/groups.json", "v2", nil, opts) + if err != nil { + return nil, err + } + + var groups []Group + err = json.Unmarshal(msg.Body, &groups) + if err != nil { + return nil, err + } + return groups, nil +} + +// CreateGroup Creates a new Passbolt Group +func (c *Client) CreateGroup(ctx context.Context, group Group) (*Group, error) { + msg, err := c.DoCustomRequest(ctx, "POST", "/groups.json", "v2", group, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &group) + if err != nil { + return nil, err + } + return &group, nil +} + +// GetGroup gets a Passbolt Group +func (c *Client) GetGroup(ctx context.Context, groupID string) (*Group, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/groups/"+groupID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var group Group + err = json.Unmarshal(msg.Body, &group) + if err != nil { + return nil, err + } + return &group, nil +} + +// UpdateGroup Updates a existing Passbolt Group +func (c *Client) UpdateGroup(ctx context.Context, groupID string, group Group) (*Group, error) { + msg, err := c.DoCustomRequest(ctx, "PUT", "/groups/"+groupID+".json", "v2", group, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &group) + if err != nil { + return nil, err + } + return &group, nil +} + +// DeleteGroup Deletes a Passbolt Group +func (c *Client) DeleteGroup(ctx context.Context, groupID string) error { + _, err := c.DoCustomRequest(ctx, "DELETE", "/groups/"+groupID+".json", "v2", nil, nil) + if err != nil { + return err + } + return nil +} diff --git a/healthcheck.go b/healthcheck.go new file mode 100644 index 0000000..ff45226 --- /dev/null +++ b/healthcheck.go @@ -0,0 +1,26 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// PerformHealthCheck performs a Health Check +func (c *Client) PerformHealthCheck(ctx context.Context) (json.RawMessage, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/healthcheck.json", "v2", nil, nil) + if err != nil { + return nil, err + } + + return msg.Body, nil +} + +// GetHealthCheckStatus gets the Server Status +func (c *Client) GetHealthCheckStatus(ctx context.Context) (string, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/healthcheck/status.json", "v2", nil, nil) + if err != nil { + return "", err + } + + return string(msg.Body), nil +} diff --git a/helper/folder.go b/helper/folder.go new file mode 100644 index 0000000..d31d4c9 --- /dev/null +++ b/helper/folder.go @@ -0,0 +1,33 @@ +package helper + +import ( + "context" + + "github.com/speatzle/go-passbolt" +) + +func CreateFolder(ctx context.Context, c *passbolt.Client, folderParentID, name string) (string, error) { + f, err := c.CreateFolder(ctx, passbolt.Folder{ + Name: name, + FolderParentID: folderParentID, + }) + return f.ID, err +} + +func GetFolder(ctx context.Context, c *passbolt.Client, folderID string) (string, string, error) { + f, err := c.GetFolder(ctx, folderID) + return f.FolderParentID, f.Name, err +} + +func UpdateFolder(ctx context.Context, c *passbolt.Client, folderID, name string) error { + _, err := c.UpdateFolder(ctx, folderID, passbolt.Folder{Name: name}) + return err +} + +func DeleteFolder(ctx context.Context, c *passbolt.Client, folderID string) error { + return c.DeleteFolder(ctx, folderID) +} + +func MoveFolder(ctx context.Context, c *passbolt.Client, folderID, folderParentID string) error { + return c.MoveFolder(ctx, folderID, folderParentID) +} diff --git a/helper/resources.go b/helper/resources.go new file mode 100644 index 0000000..268206f --- /dev/null +++ b/helper/resources.go @@ -0,0 +1,208 @@ +package helper + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/speatzle/go-passbolt" +) + +// CreateResource Creates a Resource where the Password and Description are Encrypted and Returns the Resources ID +func CreateResource(ctx context.Context, c *passbolt.Client, folderParentID, name, username, uri, password, description string) (string, error) { + types, err := c.GetResourceTypes(ctx, nil) + if err != nil { + return "", fmt.Errorf("Getting ResourceTypes: %w", err) + } + var rType *passbolt.ResourceType + for _, tmp := range types { + if tmp.Slug == "password-and-description" { + rType = &tmp + } + } + if rType == nil { + return "", fmt.Errorf("Cannot find Resource type password-and-description") + } + + resource := passbolt.Resource{ + ResourceTypeID: rType.ID, + FolderParentID: folderParentID, + Name: name, + Username: username, + URI: uri, + } + + tmp := passbolt.SecretDataTypePasswordAndDescription{ + Password: password, + Description: description, + } + secretData, err := json.Marshal(&tmp) + if err != nil { + return "", fmt.Errorf("Marshalling Secret Data: %w", err) + } + + encSecretData, err := c.EncryptMessage(string(secretData)) + if err != nil { + return "", fmt.Errorf("Encrypting Secret Data for User me: %w", err) + } + resource.Secrets = []passbolt.Secret{{Data: encSecretData}} + + newresource, err := c.CreateResource(ctx, resource) + if err != nil { + return "", fmt.Errorf("Creating Resource: %w", err) + } + return newresource.ID, nil +} + +// CreateResourceSimple Creates a Legacy Resource where only the Password is Encrypted and Returns the Resources ID +func CreateResourceSimple(ctx context.Context, c *passbolt.Client, folderParentID, name, username, uri, password, description string) (string, error) { + enc, err := c.EncryptMessage(password) + if err != nil { + return "", fmt.Errorf("Encrypting Password: %w", err) + } + + res := passbolt.Resource{ + Name: name, + URI: uri, + Username: username, + FolderParentID: folderParentID, + Description: description, + Secrets: []passbolt.Secret{ + {Data: enc}, + }, + } + + resource, err := c.CreateResource(ctx, res) + if err != nil { + return "", fmt.Errorf("Creating Resource: %w", err) + } + return resource.ID, nil +} + +// GetResource Gets a Resource by ID +func GetResource(ctx context.Context, c *passbolt.Client, resourceID string) (folderParentID, name, username, uri, password, description string, err error) { + resource, err := c.GetResource(ctx, resourceID) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Getting Resource: %w", err) + } + + rType, err := c.GetResourceType(ctx, resource.ResourceTypeID) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Getting ResourceType: %w", err) + } + secret, err := c.GetSecret(ctx, resource.ID) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Getting Resource Secret: %w", err) + } + var pw string + var desc string + switch rType.Slug { + case "password-string": + pw, err = c.DecryptMessage(secret.Data) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Decrypting Secret Data: %w", err) + } + desc = resource.Description + case "password-and-description": + rawSecretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Decrypting Secret Data: %w", err) + } + + var secretData passbolt.SecretDataTypePasswordAndDescription + err = json.Unmarshal([]byte(rawSecretData), &secretData) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Parsing Decrypted Secret Data: %w", err) + } + pw = secretData.Password + desc = secretData.Description + default: + return "", "", "", "", "", "", fmt.Errorf("Unknown ResourceType: %v", rType.Slug) + } + return resource.FolderParentID, resource.Name, resource.Username, resource.URI, pw, desc, nil +} + +// UpdateResource Updates all Fields. +// Note if you want to Change the FolderParentID please use the MoveResource Function +func UpdateResource(ctx context.Context, c *passbolt.Client, resourceID, name, username, uri, password, description string) error { + resource, err := c.GetResource(ctx, resourceID) + if err != nil { + return fmt.Errorf("Getting Resource: %w", err) + } + + rType, err := c.GetResourceType(ctx, resource.ResourceTypeID) + if err != nil { + return fmt.Errorf("Getting ResourceType: %w", err) + } + + opts := &passbolt.GetUsersOptions{ + FilterHasAccess: resourceID, + } + users, err := c.GetUsers(ctx, opts) + if err != nil { + return fmt.Errorf("Getting Users: %w", err) + } + + newResource := passbolt.Resource{ + ID: resourceID, + // This needs to be specified or it will revert to a legacy password + ResourceTypeID: resource.ResourceTypeID, + Name: name, + Username: username, + URI: uri, + } + + var secretData string + switch rType.Slug { + case "password-string": + newResource.Description = description + secretData = password + case "password-and-description": + tmp := passbolt.SecretDataTypePasswordAndDescription{ + Password: password, + Description: description, + } + res, err := json.Marshal(&tmp) + if err != nil { + return fmt.Errorf("Marshalling Secret Data: %w", err) + } + secretData = string(res) + default: + return fmt.Errorf("Unknown ResourceType: %v", rType.Slug) + } + + newResource.Secrets = []passbolt.Secret{} + for _, user := range users { + var encSecretData string + // if this is our user use our stored and verified public key instead + if user.ID == c.GetUserID() { + encSecretData, err = c.EncryptMessage(secretData) + if err != nil { + return fmt.Errorf("Encrypting Secret Data for User me: %w", err) + } + } else { + encSecretData, err = c.EncryptMessageWithPublicKey(user.GPGKey.ArmoredKey, secretData) + if err != nil { + return fmt.Errorf("Encrypting Secret Data for User %v: %w", user.ID, err) + } + } + newResource.Secrets = append(newResource.Secrets, passbolt.Secret{ + UserID: user.ID, + Data: encSecretData, + }) + } + + _, err = c.UpdateResource(ctx, resourceID, newResource) + if err != nil { + return fmt.Errorf("Updating Resource: %w", err) + } + return nil +} + +func DeleteResource(ctx context.Context, c *passbolt.Client, resourceID string) error { + return c.DeleteResource(ctx, resourceID) +} + +func MoveResource(ctx context.Context, c *passbolt.Client, resourceID, folderParentID string) error { + return c.MoveResource(ctx, resourceID, folderParentID) +} diff --git a/helper/share.go b/helper/share.go new file mode 100644 index 0000000..4f4c677 --- /dev/null +++ b/helper/share.go @@ -0,0 +1,218 @@ +package helper + +import ( + "context" + "fmt" + + "github.com/speatzle/go-passbolt" +) + +// ShareOperation defines how Resources are to be Shared With Users/Groups +type ShareOperation struct { + // Type of Permission: 1 = Read, 7 = can Update, 15 = Owner (Owner can also Share Resource) + // Note: Setting this to -1 Will delete this Permission if it already Exists, errors if this Permission Dosen't Already Exists + Type int + // ARO is what Type this should be Shared With (User, Group) + ARO string + // AROID is the ID of the User or Group(ARO) this should be Shared With + AROID string +} + +// ShareResourceWithUsersAndGroups Shares a Resource With The Users and Groups with the Specified Permission Type, +// if the Resource has already been shared With the User/Group the Permission Type will be Adjusted/Deleted +func ShareResourceWithUsersAndGroups(ctx context.Context, c *passbolt.Client, resourceID string, Users []string, Groups []string, permissionType int) error { + changes := []ShareOperation{} + for _, userID := range Users { + changes = append(changes, ShareOperation{ + Type: permissionType, + ARO: "User", + AROID: userID, + }) + } + for _, groupID := range Groups { + changes = append(changes, ShareOperation{ + Type: permissionType, + ARO: "Group", + AROID: groupID, + }) + } + return ShareResource(ctx, c, resourceID, changes) +} + +// ShareResource Shares a Resource as Specified in the Passed ShareOperation Struct Slice +func ShareResource(ctx context.Context, c *passbolt.Client, resourceID string, changes []ShareOperation) error { + oldPermissions, err := c.GetResourcePermissions(ctx, resourceID) + if err != nil { + return fmt.Errorf("Getting Resource Permissions: %w", err) + } + + permissionChanges, err := GeneratePermissionChanges(oldPermissions, changes) + if err != nil { + return fmt.Errorf("Generating Resource Permission Changes: %w", err) + } + + shareRequest := passbolt.ResourceShareRequest{Permissions: permissionChanges} + + secret, err := c.GetSecret(ctx, resourceID) + if err != nil { + return fmt.Errorf("Get Resource: %w", err) + } + + secretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return fmt.Errorf("Decrypting Resource Secret: %w", err) + } + + simulationResult, err := c.SimulateShareResource(ctx, resourceID, shareRequest) + if err != nil { + return fmt.Errorf("Simulate Share Resource: %w", err) + } + + users, err := c.GetUsers(ctx, nil) + if err != nil { + return fmt.Errorf("Get Users: %w", err) + } + + shareRequest.Secrets = []passbolt.Secret{} + for _, user := range simulationResult.Changes.Added { + pubkey, err := getPublicKeyByUserID(user.User.ID, users) + if err != nil { + return fmt.Errorf("Getting Public Key for User %v: %w", user.User.ID, err) + } + + encSecretData, err := c.EncryptMessageWithPublicKey(pubkey, secretData) + if err != nil { + return fmt.Errorf("Encrypting Secret for User %v: %w", user.User.ID, err) + } + shareRequest.Secrets = append(shareRequest.Secrets, passbolt.Secret{ + UserID: user.User.ID, + Data: encSecretData, + }) + } + + err = c.ShareResource(ctx, resourceID, shareRequest) + if err != nil { + return fmt.Errorf("Sharing Resource: %w", err) + } + return nil +} + +// ShareFolderWithUsersAndGroups Shares a Folder With The Users and Groups with the Specified Type, +// if the Folder has already been shared With the User/Group the Permission Type will be Adjusted/Deleted. +// Note: Resources Permissions in the Folder are not Adjusted (Like the Extention does) +func ShareFolderWithUsersAndGroups(ctx context.Context, c *passbolt.Client, folderID string, Users []string, Groups []string, permissionType int) error { + changes := []ShareOperation{} + for _, userID := range Users { + changes = append(changes, ShareOperation{ + Type: permissionType, + ARO: "User", + AROID: userID, + }) + } + for _, groupID := range Groups { + changes = append(changes, ShareOperation{ + Type: permissionType, + ARO: "Group", + AROID: groupID, + }) + } + return ShareFolder(ctx, c, folderID, changes) +} + +// ShareFolder Shares a Folder as Specified in the Passed ShareOperation Struct Slice. +// Note Resources Permissions in the Folder are not Adjusted +func ShareFolder(ctx context.Context, c *passbolt.Client, folderID string, changes []ShareOperation) error { + oldPermissions, err := c.GetFolderPermissions(ctx, folderID) + if err != nil { + return fmt.Errorf("Getting Folder Permissions: %w", err) + } + + permissionChanges, err := GeneratePermissionChanges(oldPermissions, changes) + if err != nil { + return fmt.Errorf("Generating Folder Permission Changes: %w", err) + } + + err = c.ShareFolder(ctx, folderID, permissionChanges) + if err != nil { + return fmt.Errorf("Sharing Folder: %w", err) + } + return nil +} + +// GeneratePermissionChanges Generates the Permission Changes for a Resource/Folder nessesary for a single Share Operation +func GeneratePermissionChanges(oldPermissions []passbolt.Permission, changes []ShareOperation) ([]passbolt.Permission, error) { + // Check for Duplicate Users/Groups as that would break stuff + for i, changeA := range changes { + for j, changeB := range changes { + if i != j && changeA.AROID == changeB.AROID && changeA.ARO == changeB.ARO { + return nil, fmt.Errorf("Change %v and %v are Both About the same ARO %v ID: %v, there can only be once change per ARO", i, j, changeA.ARO, changeA.AROID) + } + } + } + + // Get ACO and ACO ID from Existing Permissions + if len(oldPermissions) == 0 { + return nil, fmt.Errorf("There has to be atleast one Permission on a ACO") + } + ACO := oldPermissions[0].ACO + ACOID := oldPermissions[0].ACOForeignKey + + permissionChanges := []passbolt.Permission{} + for _, change := range changes { + // Find Permission thats invloves the Same ARO as Requested in the change + var oldPermission *passbolt.Permission + for _, oldPerm := range oldPermissions { + if oldPerm.ARO == change.ARO && oldPerm.AROForeignKey == change.AROID { + oldPermission = &oldPerm + } + } + // Check Wheter Matching Permission Already Exists and needs to be adjusted or is a new one can be created + if oldPermission == nil { + if change.Type == 15 || change.Type == 7 || change.Type == 1 { + permissionChanges = append(permissionChanges, passbolt.Permission{ + IsNew: true, + Type: change.Type, + ARO: change.ARO, + AROForeignKey: change.AROID, + ACO: ACO, + ACOForeignKey: ACOID, + }) + } else if change.Type == -1 { + return nil, fmt.Errorf("Permission for %v %v Cannot be Deleted as No Matching Permission Exists", change.ARO, change.AROID) + } else { + return nil, fmt.Errorf("Unknown Permission Type: %v", change.Type) + } + } else { + tmp := passbolt.Permission{ + ID: oldPermission.ID, + ARO: change.ARO, + AROForeignKey: change.AROID, + ACO: ACO, + ACOForeignKey: ACOID, + } + + if change.Type == 15 || change.Type == 7 || change.Type == 1 { + if oldPermission.Type == change.Type { + return nil, fmt.Errorf("Permission for %v %v is already Type %v", change.ARO, change.AROID, change.Type) + } + tmp.Type = change.Type + } else if change.Type == -1 { + tmp.Delete = true + tmp.Type = oldPermission.Type + } else { + return nil, fmt.Errorf("Unknown Permission Type: %v", change.Type) + } + permissionChanges = append(permissionChanges, tmp) + } + } + return permissionChanges, nil +} + +func getPublicKeyByUserID(userID string, Users []passbolt.User) (string, error) { + for _, user := range Users { + if user.ID == userID { + return user.GPGKey.ArmoredKey, nil + } + } + return "", fmt.Errorf("Cannot Find Key for user id %v", userID) +} diff --git a/misc.go b/misc.go new file mode 100644 index 0000000..2d7f595 --- /dev/null +++ b/misc.go @@ -0,0 +1,25 @@ +package passbolt + +import ( + "crypto/rand" + "math/big" +) + +func randStringBytesRmndr(length int) (string, error) { + result := "" + for { + if len(result) >= length { + return result, nil + } + num, err := rand.Int(rand.Reader, big.NewInt(int64(127))) + if err != nil { + return "", err + } + n := num.Int64() + // Make sure that the number/byte/letter is inside + // the range of printable ASCII characters (excluding space and DEL) + if n > 32 && n < 127 { + result += string(n) + } + } +} diff --git a/permissions.go b/permissions.go new file mode 100644 index 0000000..88ad8b3 --- /dev/null +++ b/permissions.go @@ -0,0 +1,50 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// Permission is a Permission +type Permission struct { + ID string `json:"id,omitempty"` + ACO string `json:"aco,omitempty"` + ARO string `json:"aro,omitempty"` + ACOForeignKey string `json:"aco_foreign_key,omitempty"` + AROForeignKey string `json:"aro_foreign_key,omitempty"` + Type int `json:"type,omitempty"` + Delete bool `json:"delete,omitempty"` + IsNew bool `json:"is_new,omitempty"` + Created *Time `json:"created,omitempty"` + Modified *Time `json:"modified,omitempty"` +} + +// GetResourcePermissions gets a Resources Permissions +func (c *Client) GetResourcePermissions(ctx context.Context, resourceID string) ([]Permission, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/permissions/resource/"+resourceID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var permissions []Permission + err = json.Unmarshal(msg.Body, &permissions) + if err != nil { + return nil, err + } + return permissions, nil +} + +// GetFolderPermissions gets a Folders Permissions +func (c *Client) GetFolderPermissions(ctx context.Context, folderID string) ([]Permission, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/permissions/folder/"+folderID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var permissions []Permission + err = json.Unmarshal(msg.Body, &permissions) + if err != nil { + return nil, err + } + return permissions, nil +} diff --git a/resource_types.go b/resource_types.go new file mode 100644 index 0000000..0581cbf --- /dev/null +++ b/resource_types.go @@ -0,0 +1,49 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +//ResourceType is the Type of a Resource +type ResourceType struct { + ID string `json:"id,omitempty"` + Slug string `json:"slug,omitempty"` + Description string `json:"description,omitempty"` + Definition json.RawMessage `json:"definition,omitempty"` + Created *Time `json:"created,omitempty"` + Modified *Time `json:"modified,omitempty"` +} + +type GetResourceTypesOptions struct { +} + +// GetResourceTypes gets all Passbolt Resource Types +func (c *Client) GetResourceTypes(ctx context.Context, opts *GetResourceTypesOptions) ([]ResourceType, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/resource-types.json", "v2", nil, opts) + if err != nil { + return nil, err + } + + var types []ResourceType + err = json.Unmarshal(msg.Body, &types) + if err != nil { + return nil, err + } + return types, nil +} + +// GetResourceType gets a Passbolt Type +func (c *Client) GetResourceType(ctx context.Context, typeID string) (*ResourceType, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/resource-types/"+typeID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var rType ResourceType + err = json.Unmarshal(msg.Body, &rType) + if err != nil { + return nil, err + } + return &rType, nil +} diff --git a/resources.go b/resources.go new file mode 100644 index 0000000..0e9bcda --- /dev/null +++ b/resources.go @@ -0,0 +1,135 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// Resource is a Resource. +// Warning: Since Passbolt v3 some fields here may not be populated as they may be in the Secret depending on the ResourceType, +// for now the only Field like that is the Decription. +type Resource struct { + ID string `json:"id,omitempty"` + Created *Time `json:"created,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + Creator *User `json:"creator,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Description string `json:"description,omitempty"` + Favorite *Favorite `json:"favorite,omitempty"` + Modified *Time `json:"modified,omitempty"` + ModifiedBy string `json:"modified_by,omitempty"` + Modifier *User `json:"modifier,omitempty"` + Name string `json:"name,omitempty"` + Permission *Permission `json:"permission,omitempty"` + URI string `json:"uri,omitempty"` + Username string `json:"username,omitempty"` + FolderParentID string `json:"folder_parent_id,omitempty"` + ResourceTypeID string `json:"resource_type_id,omitempty"` + Secrets []Secret `json:"secrets,omitempty"` + Tags []Tag `json:"tags,omitempty"` +} + +// Tag is a Passbolt Password Tag +type Tag struct { + ID string `json:"id,omitempty"` + Slug string `json:"slug,omitempty"` + IsShared bool `json:"is_shared,omitempty"` +} + +// GetResourcesOptions are all available query parameters +type GetResourcesOptions struct { + FilterIsFavorite bool `url:"filter[is-favorite],omitempty"` + FilterIsSharedWithMe bool `url:"filter[is-shared-with-me],omitempty"` + FilterIsSharedWithGroup string `url:"filter[is-shared-with-group],omitempty"` + FilterHasID string `url:"filter[has-id],omitempty"` + // Parent Folder id + FilterHasParent string `url:"filter[has-parent],omitempty"` + FilterHasTag string `url:"filter[has-tag],omitempty"` + + ContainCreator bool `url:"contain[creator],omitempty"` + ContainFavorites bool `url:"contain[favorite],omitempty"` + ContainModifier bool `url:"contain[modifier],omitempty"` + ContainPermissions bool `url:"contain[permission],omitempty"` + ContainPermissionsUserProfile bool `url:"contain[permissions.user.profile],omitempty"` + ContainPermissionsGroup bool `url:"contain[permissions.group],omitempty"` + ContainSecret bool `url:"contain[secret],omitempty"` + ContainTags bool `url:"contain[tag],omitempty"` +} + +// GetResources gets all Passbolt Resources +func (c *Client) GetResources(ctx context.Context, opts *GetResourcesOptions) ([]Resource, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/resources.json", "v2", nil, opts) + if err != nil { + return nil, err + } + + var resources []Resource + err = json.Unmarshal(msg.Body, &resources) + if err != nil { + return nil, err + } + return resources, nil +} + +// CreateResource Creates a new Passbolt Resource +func (c *Client) CreateResource(ctx context.Context, resource Resource) (*Resource, error) { + msg, err := c.DoCustomRequest(ctx, "POST", "/resources.json", "v2", resource, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &resource) + if err != nil { + return nil, err + } + return &resource, nil +} + +// GetResource gets a Passbolt Resource +func (c *Client) GetResource(ctx context.Context, resourceID string) (*Resource, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/resources/"+resourceID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var resource Resource + err = json.Unmarshal(msg.Body, &resource) + if err != nil { + return nil, err + } + return &resource, nil +} + +// UpdateResource Updates a existing Passbolt Resource +func (c *Client) UpdateResource(ctx context.Context, resourceID string, resource Resource) (*Resource, error) { + msg, err := c.DoCustomRequest(ctx, "PUT", "/resources/"+resourceID+".json", "v2", resource, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &resource) + if err != nil { + return nil, err + } + return &resource, nil +} + +// DeleteResource Deletes a Passbolt Resource +func (c *Client) DeleteResource(ctx context.Context, resourceID string) error { + _, err := c.DoCustomRequest(ctx, "DELETE", "/resources/"+resourceID+".json", "v2", nil, nil) + if err != nil { + return err + } + return nil +} + +// MoveResource Moves a Passbolt Resource +func (c *Client) MoveResource(ctx context.Context, resourceID, folderParentID string) error { + _, err := c.DoCustomRequest(ctx, "PUT", "/move/resource/"+resourceID+".json", "v2", Resource{ + FolderParentID: folderParentID, + }, nil) + if err != nil { + return err + } + return nil +} diff --git a/roles.go b/roles.go new file mode 100644 index 0000000..81c4c72 --- /dev/null +++ b/roles.go @@ -0,0 +1,55 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +//Role is a Role +type Role struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Created *Time `json:"created,omitempty"` + Description string `json:"description,omitempty"` + Modified *Time `json:"modified,omitempty"` + Avatar Avatar `json:"avatar,omitempty"` +} + +// Avatar is a Users Avatar +type Avatar struct { + ID string `json:"id,omitempty"` + UserID string `json:"user_id,omitempty"` + ForeignKey string `json:"foreign_key,omitempty"` + Model string `json:"model,omitempty"` + Filename string `json:"filename,omitempty"` + Filesize int `json:"filesize,omitempty"` + MimeType string `json:"mime_type,omitempty"` + Extension string `json:"extension,omitempty"` + Hash string `json:"hash,omitempty"` + Path string `json:"path,omitempty"` + Adapter string `json:"adapter,omitempty"` + Created *Time `json:"created,omitempty"` + Modified *Time `json:"modified,omitempty"` + URL *URL `json:"url,omitempty"` +} + +// URL is a Passbolt URL +type URL struct { + Medium string `json:"medium,omitempty"` + Small string `json:"small,omitempty"` +} + +// GetRoles gets all Passbolt Roles +func (c *Client) GetRoles(ctx context.Context) ([]Role, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/roles.json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var roles []Role + err = json.Unmarshal(msg.Body, &roles) + if err != nil { + return nil, err + } + return roles, nil +} diff --git a/secrets.go b/secrets.go new file mode 100644 index 0000000..21bb6c3 --- /dev/null +++ b/secrets.go @@ -0,0 +1,36 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// Secret is a Secret +type Secret struct { + ID string `json:"id,omitempty"` + UserID string `json:"user_id,omitempty"` + ResourceID string `json:"resource_id,omitempty"` + Data string `json:"data,omitempty"` + Created *Time `json:"created,omitempty"` + Modified *Time `json:"modified,omitempty"` +} + +type SecretDataTypePasswordAndDescription struct { + Password string `json:"password"` + Description string `json:"description,omitempty"` +} + +// GetSecret gets a Passbolt Secret +func (c *Client) GetSecret(ctx context.Context, resourceID string) (*Secret, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/secrets/resource/"+resourceID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var secret Secret + err = json.Unmarshal(msg.Body, &secret) + if err != nil { + return nil, err + } + return &secret, nil +} diff --git a/share.go b/share.go new file mode 100644 index 0000000..641bdf5 --- /dev/null +++ b/share.go @@ -0,0 +1,93 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// ResourceShareRequest is a ResourceShareRequest +type ResourceShareRequest struct { + Permissions []Permission `json:"permissions,omitempty"` + Secrets []Secret `json:"secrets,omitempty"` +} + +// ResourceShareSimulationResult is the Result of a Sharing Siumulation +type ResourceShareSimulationResult struct { + Changes ResourceShareSimulationChanges `json:"changes,omitempty"` +} + +type ResourceShareSimulationChanges struct { + Added []ResourceShareSimulationChange `json:"added,omitempty"` + Removed []ResourceShareSimulationChange `json:"removed,omitempty"` +} + +type ResourceShareSimulationChange struct { + User ResourceShareSimulationUser `json:"user,omitempty"` +} + +type ResourceShareSimulationUser struct { + ID string `json:"id,omitempty"` +} + +// ARO is a User or a Group +type ARO struct { + User + Group +} + +// SearchAROsOptions are all available query parameters +type SearchAROsOptions struct { + FilterSearch string `url:"filter[search],omitempty"` +} + +// SearchAROs gets all Passbolt AROs +func (c *Client) SearchAROs(ctx context.Context, opts SearchAROsOptions) ([]ARO, error) { + //set is_new to true in permission + msg, err := c.DoCustomRequest(ctx, "GET", "/share/search-aros.json", "v2", nil, opts) + if err != nil { + return nil, err + } + + var aros []ARO + err = json.Unmarshal(msg.Body, &aros) + if err != nil { + return nil, err + } + return aros, nil +} + +// ShareResource Shares a Resource with AROs +func (c *Client) ShareResource(ctx context.Context, resourceID string, shareRequest ResourceShareRequest) error { + _, err := c.DoCustomRequest(ctx, "PUT", "/share/resource/"+resourceID+".json", "v2", shareRequest, nil) + if err != nil { + return err + } + + return nil +} + +// ShareFolder Shares a Folder with AROs +func (c *Client) ShareFolder(ctx context.Context, folderID string, permissions []Permission) error { + f := Folder{Permissions: permissions} + _, err := c.DoCustomRequest(ctx, "PUT", "/share/folder/"+folderID+".json", "v2", f, nil) + if err != nil { + return err + } + + return nil +} + +// SimulateShareResource Simulates Shareing a Resource with AROs +func (c *Client) SimulateShareResource(ctx context.Context, resourceID string, shareRequest ResourceShareRequest) (*ResourceShareSimulationResult, error) { + msg, err := c.DoCustomRequest(ctx, "POST", "/share/simulate/resource/"+resourceID+".json", "v2", shareRequest, nil) + if err != nil { + return nil, err + } + + var res ResourceShareSimulationResult + err = json.Unmarshal(msg.Body, &res) + if err != nil { + return nil, err + } + return &res, nil +} diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 0000000..219a9ed --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1,10 @@ +checks = ["all", "-ST1005", "-ST1000", "-ST1003", "-ST1016"] +initialisms = ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", + "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", + "IP", "JSON", "QPS", "RAM", "RPC", "SLA", + "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", + "UDP", "UI", "GID", "UID", "UUID", "URI", + "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", + "XSS"] +dot_import_whitelist = [] +http_status_code_whitelist = ["200", "400", "404", "500"] \ No newline at end of file diff --git a/time.go b/time.go new file mode 100644 index 0000000..da63ce2 --- /dev/null +++ b/time.go @@ -0,0 +1,29 @@ +package passbolt + +import ( + "strings" + "time" +) + +// Time is here to unmarshall time correctly +type Time struct { + time.Time +} + +// UnmarshalJSON Parses Passbolt *Time +func (t *Time) UnmarshalJSON(buf []byte) error { + if string(buf) == "null" { + return nil + } + tt, err := time.Parse(time.RFC3339, strings.Trim(string(buf), `"`)) + if err != nil { + return err + } + t.Time = tt + return nil +} + +// MarshalJSON Marshals Passbolt *Time +func (t Time) MarshalJSON() ([]byte, error) { + return []byte(`"` + t.Time.Format(time.RFC3339) + `"`), nil +} diff --git a/users.go b/users.go new file mode 100644 index 0000000..1b24d9d --- /dev/null +++ b/users.go @@ -0,0 +1,124 @@ +package passbolt + +import ( + "context" + "encoding/json" +) + +// User contains information about a passbolt User +type User struct { + ID string `json:"id,omitempty"` + Created *Time `json:"created,omitempty"` + Active bool `json:"active,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Description string `json:"description,omitempty"` + Favorite *Favorite `json:"favorite,omitempty"` + Modified *Time `json:"modified,omitempty"` + Username string `json:"username,omitempty"` + RoleID string `json:"role_id,omitempty"` + Profile *Profile `json:"profile,omitempty"` + Role *Role `json:"role,omitempty"` + GPGKey *GPGKey `json:"gpgKey,omitempty"` + LastLoggedIn string `json:"last_logged_in,omitempty"` +} + +// Profile is a Profile +type Profile struct { + ID string `json:"id,omitempty"` + UserID string `json:"user_id,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Created *Time `json:"created,omitempty"` + Modified *Time `json:"modified,omitempty"` +} + +// GetUsersOptions are all available query parameters +type GetUsersOptions struct { + FilterSearch string `url:"filter[search],omitempty"` + FilterHasGroup string `url:"filter[has-group],omitempty"` + FilterHasAccess string `url:"filter[has-access],omitempty"` + FilterIsAdmin bool `url:"filter[is-admin],omitempty"` + + ContainLastLoggedIn bool `url:"contain[LastLoggedIn],omitempty"` +} + +// GetUsers gets all Passbolt Users +func (c *Client) GetUsers(ctx context.Context, opts *GetUsersOptions) ([]User, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/users.json", "v2", nil, opts) + if err != nil { + return nil, err + } + + var users []User + err = json.Unmarshal(msg.Body, &users) + if err != nil { + return nil, err + } + return users, nil +} + +// CreateUser Creates a new Passbolt User +func (c *Client) CreateUser(ctx context.Context, user User) (*User, error) { + msg, err := c.DoCustomRequest(ctx, "POST", "/users.json", "v2", user, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &user) + if err != nil { + return nil, err + } + return &user, nil +} + +// GetMe gets the currently logged in Passbolt User +func (c *Client) GetMe(ctx context.Context) (*User, error) { + return c.GetUser(ctx, "me") +} + +// GetUser gets a Passbolt User +func (c *Client) GetUser(ctx context.Context, userID string) (*User, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "/users/"+userID+".json", "v2", nil, nil) + if err != nil { + return nil, err + } + + var user User + err = json.Unmarshal(msg.Body, &user) + if err != nil { + return nil, err + } + return &user, nil +} + +// UpdateUser Updates a existing Passbolt User +func (c *Client) UpdateUser(ctx context.Context, userID string, user User) (*User, error) { + msg, err := c.DoCustomRequest(ctx, "PUT", "/users/"+userID+".json", "v2", user, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(msg.Body, &user) + if err != nil { + return nil, err + } + return &user, nil +} + +// DeleteUser Deletes a Passbolt User +func (c *Client) DeleteUser(ctx context.Context, userID string) error { + _, err := c.DoCustomRequest(ctx, "DELETE", "/users/"+userID+".json", "v2", nil, nil) + if err != nil { + return err + } + return nil +} + +// DeleteUserDryrun Check if a Passbolt User is Deleteable +func (c *Client) DeleteUserDryrun(ctx context.Context, userID string) error { + _, err := c.DoCustomRequest(ctx, "DELETE", "/users/"+userID+"/dry-run.json", "v2", nil, nil) + if err != nil { + return err + } + return nil +}