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 +}