mirror of
https://github.com/passbolt/go-passbolt.git
synced 2025-05-13 19:18:20 +00:00
Compare commits
98 commits
Author | SHA1 | Date | |
---|---|---|---|
b919c3e09d | |||
3762c5e278 | |||
26942f22d1 | |||
![]() |
c1904ca20a | ||
ddaa090bc7 | |||
c86afac97c | |||
2b3bb48385 | |||
83ba14250b | |||
390b7be866 | |||
aaf7213315 | |||
1bbe7dc952 | |||
1499a80625 | |||
e13f484bcb | |||
360cc3748e | |||
a6a98a6887 | |||
8dbb07720d | |||
605db2b047 | |||
adaffbce7e | |||
7316263056 | |||
876631e7c2 | |||
a9bd51e5da | |||
ce38d65e45 | |||
e4537a8ca0 | |||
![]() |
1b178b6634 | ||
![]() |
a86ae886f2 | ||
![]() |
fd895a9d46 | ||
![]() |
ced16f2479 | ||
![]() |
1aaafcf66e | ||
![]() |
8a5cbff839 | ||
![]() |
5ce26cfbfc | ||
![]() |
4d482e6bf2 | ||
![]() |
563c755168 | ||
![]() |
9e181e6c83 | ||
![]() |
df43c781ad | ||
![]() |
1b30521b5b | ||
![]() |
69221b3a24 | ||
![]() |
47ec059232 | ||
![]() |
a3a55e2199 | ||
![]() |
bd2460467d | ||
![]() |
9cc8cc8b02 | ||
![]() |
d18f38c066 | ||
![]() |
90b10d5570 | ||
![]() |
5bab492d89 | ||
![]() |
0a86e0c1e6 | ||
![]() |
f1122a019c | ||
![]() |
f3fe6eb1c5 | ||
![]() |
d84e7e7ad7 | ||
![]() |
b0187ae821 | ||
![]() |
f790467d28 | ||
![]() |
8e4637492d | ||
![]() |
0e7cca97a2 | ||
![]() |
6eb5734169 | ||
![]() |
43193345fa | ||
![]() |
8bccb80cb2 | ||
![]() |
151bd9643b | ||
![]() |
7fdad5269b | ||
![]() |
663f5f6b76 | ||
![]() |
f01926b1c5 | ||
![]() |
21c833b742 | ||
![]() |
c3f7f9ac1b | ||
![]() |
27715fd266 | ||
![]() |
f6b82dcbe9 | ||
![]() |
940828965a | ||
![]() |
9bceb71ed2 | ||
![]() |
7e96b96c36 | ||
![]() |
f3a8d4a05f | ||
![]() |
5fbed0b4d5 | ||
![]() |
bdd57b482e | ||
![]() |
444a3d0583 | ||
![]() |
8381328ea9 | ||
![]() |
f3e40caa8f | ||
![]() |
815df3e9f3 | ||
![]() |
6bb4283e5e | ||
![]() |
824e0f7ccf | ||
![]() |
ea631ac6a5 | ||
![]() |
00dbd90175 | ||
![]() |
c8e7981f4b | ||
![]() |
0beee61585 | ||
![]() |
8a3b33ba98 | ||
![]() |
54f4b4ca23 | ||
![]() |
28e2ccf7e4 | ||
![]() |
0538e86ddd | ||
![]() |
e5e0a16010 | ||
![]() |
492cc57a97 | ||
![]() |
716e537f24 | ||
![]() |
39dc7c833d | ||
![]() |
8c2be0f37f | ||
![]() |
3f4ed25a83 | ||
![]() |
279d245d86 | ||
![]() |
1de6f6a815 | ||
![]() |
1fbe48be83 | ||
![]() |
ae4541d66e | ||
![]() |
3325eecb97 | ||
![]() |
a18a6bac6f | ||
![]() |
0731d52273 | ||
![]() |
19533e3409 | ||
![]() |
866ca4e307 | ||
![]() |
2a08213258 |
36 changed files with 1803 additions and 343 deletions
34
.github/workflows/.go.yml
vendored
Normal file
34
.github/workflows/.go.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.23
|
||||
- name: "Setup Passbolt"
|
||||
run: |
|
||||
git clone https://github.com/passbolt/passbolt_docker.git ../passbolt_docker
|
||||
cd ../passbolt_docker
|
||||
docker compose -f docker-compose/docker-compose-ce.yaml up -d
|
||||
docker ps -a
|
||||
- name: "Test"
|
||||
run: |
|
||||
docker exec docker-compose-passbolt-1 sh -c '/usr/bin/wait-for.sh -t 30 localhost:443'
|
||||
output=$(docker exec docker-compose-passbolt-1 sh -c 'su -m -c "/usr/share/php/passbolt/bin/cake \
|
||||
passbolt register_user \
|
||||
-u your@email.com \
|
||||
-f yourname \
|
||||
-l surname \
|
||||
-r admin" -s /bin/sh www-data')
|
||||
export REG_URL=$(echo ${output##* your mailbox or here: } | tr -d '\n')
|
||||
echo "Register with $REG_URL"
|
||||
go test -v ./...
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 speatzle
|
||||
Copyright (c) 2021 Samuel Lorch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
225
README.md
225
README.md
|
@ -1,23 +1,34 @@
|
|||
# go-passbolt
|
||||
[](https://pkg.go.dev/github.com/speatzle/go-passbolt)
|
||||
|
||||
A Go Module to interact with [Passbolt](https://www.passbolt.com/), a Open source Password Manager for Teams
|
||||
[](https://pkg.go.dev/github.com/passbolt/go-passbolt)
|
||||
|
||||
This Module tries to Support the Latest Passbolt Community/PRO Server Release, PRO Features Such as Folders are Supported.
|
||||
Older Versions of Passbolt such as v2 are unsupported (it's a Password Manager, please update it)
|
||||
A Go module to interact with [Passbolt](https://www.passbolt.com/), an open-source password manager for teams
|
||||
|
||||
This Module is split into 2 packages: api and helper, in the api package you will find everything to directly interact with the API. The helper Package has simplified functions that use the api package to perform common but complicated tasks such as Sharing a Password. To use the API Package please read the Passbolt API [Docs](https://help.passbolt.com/api).
|
||||
Sadly the Docs aren't Complete so many Things here have been found by looking at the source of Passbolt or through trial and error, if you have a Question just ask.
|
||||
There also is a CLI Tool to interact with Passbolt using this module [here](https://github.com/passbolt/go-passbolt-cli).
|
||||
|
||||
PR's are Welcome, if it's something bigger / fundamental: Please make a Issue and ask first.
|
||||
This module tries to support the latest Passbolt Community/PRO server release, PRO Features such as folders are supported. Older versions of Passbolt such as v2 are unsupported (it's a password manager, please update it)
|
||||
|
||||
This module is divided into two packages: API and helper.
|
||||
|
||||
In the API package, you will find everything to directly interact with the API.
|
||||
|
||||
The helper package has simplified functions that use the API package to perform common but complicated tasks such as sharing a password.
|
||||
|
||||
To use the API package, please read the [Passbolt API docs](https://help.passbolt.com/api). Sadly the docs aren't complete so many things here have been found by looking at the source of Passbolt or through trial and error. If you have a question just ask.
|
||||
|
||||
PR's are welcome. But be gentle: if it's something bigger or fundamental: please [create an issue](https://github.com/passbolt/go-passbolt/issues/new) and ask first.
|
||||
|
||||
Disclaimer: This project is community driven and not associated with Passbolt SA
|
||||
|
||||
# Install
|
||||
|
||||
`go get github.com/speatzle/go-passbolt`
|
||||
`go get github.com/passbolt/go-passbolt`
|
||||
|
||||
# Examples
|
||||
|
||||
## Login
|
||||
First you will need to Create a Client, and then Login on the Server using the Client
|
||||
|
||||
First, you will need to create a client and then log in on the server using the client:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
@ -26,7 +37,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/speatzle/go-passbolt/api"
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
)
|
||||
|
||||
const address = "https://passbolt.example.com"
|
||||
|
@ -54,11 +65,14 @@ func main() {
|
|||
}
|
||||
```
|
||||
|
||||
Note: if you want to use the client for some time then you'll have to make sure it is still logged in.
|
||||
Note: if you want to use the client for a long time then you'll have to make sure it is still logged in.
|
||||
|
||||
You can do this using the `client.CheckSession()` function.
|
||||
|
||||
## Create a Resource
|
||||
Creating a Resource using the helper package is simple, first add `"github.com/speatzle/go-passbolt/helper"` to your imports.
|
||||
|
||||
Creating a resource using the helper package is simple. First, add `"github.com/passbolt/go-passbolt/helper"` to your imports.
|
||||
|
||||
Then you can simply:
|
||||
|
||||
```go
|
||||
|
@ -70,11 +84,11 @@ resourceID, err := helper.CreateResource(
|
|||
"user123", // Username
|
||||
"https://test.example.com", // URI
|
||||
"securePassword123", // Password
|
||||
"This is a Account for the example test portal", // Description
|
||||
"This is an Account for the example test portal", // Description
|
||||
)
|
||||
```
|
||||
|
||||
Creating a (Legacy) Resource Without the helper package would look like this:
|
||||
Creating a (legacy) resource without the helper package would look like this:
|
||||
|
||||
```go
|
||||
enc, err := client.EncryptMessage("securePassword123")
|
||||
|
@ -86,7 +100,7 @@ res := api.Resource{
|
|||
Name: "Example Account",
|
||||
Username: "user123",
|
||||
URI: "https://test.example.com",
|
||||
Description: "This is a Account for the example test portal",
|
||||
Description: "This is an Account for the example test portal",
|
||||
Secrets: []api.Secret{
|
||||
{Data: enc},
|
||||
},
|
||||
|
@ -98,22 +112,24 @@ if err != nil {
|
|||
}
|
||||
```
|
||||
|
||||
Note: Since Passbolt v3 There are Resource Types, this Manual Example just creates a "password-string" Type Password where the Description is Unencrypted, Read More [Here](https://help.passbolt.com/api/resource-types).
|
||||
Note: Since Passbolt v3 there are resource types. This manual example creates a "password-string" type password where the description is unencrypted. Read more [here](https://help.passbolt.com/api/resource-types).
|
||||
|
||||
## Getting
|
||||
Generally API Get Calls will have options (opts) that allow for specifing filters and contains, if you dont want to specify options just pass nil.
|
||||
Filters just filter by whatever is given, contains on the otherhand specify what to include in the response. Many Filters And Contains are undocumented in the Passbolt Docs.
|
||||
|
||||
Here We Specify that we want to Filter by Favorites and that the Response Should Contain the Permissions for each Resource:
|
||||
Generally, API GET calls will have parameters that allow specifying `filters` and `contains`, if you don't want to define those parameters just pass nil.
|
||||
|
||||
`Filters` just filter by whatever is given, `contains` on the other hand specify what information you want to include in the response. Many `filters` and `contains` are undocumented in the Passbolt docs.
|
||||
|
||||
Here we specify that we want to filter by favorites and that the response should contain the permissions for each resource:
|
||||
|
||||
```go
|
||||
favorites, err := client.GetResources(ctx, &api.GetResourcesOptions{
|
||||
FilterIsFavorite: true,
|
||||
ContainPermissions: true,
|
||||
ContainPermissions: true,
|
||||
})
|
||||
```
|
||||
|
||||
We Can do the Same for Users:
|
||||
We can do the same for users:
|
||||
|
||||
```go
|
||||
users, err := client.GetUsers(ctx, &api.GetUsersOptions{
|
||||
|
@ -126,12 +142,12 @@ Groups:
|
|||
|
||||
```go
|
||||
groups, err := client.GetGroups(ctx, &api.GetGroupsOptions{
|
||||
FilterHasUsers: []string{"id of user", "id of other user"},
|
||||
FilterHasUsers: []string{"id of user", "id of other user"},
|
||||
ContainUser: true,
|
||||
})
|
||||
```
|
||||
|
||||
And also for Folders (PRO Only):
|
||||
And also for folders (PRO only):
|
||||
|
||||
```go
|
||||
folders, err := client.GetFolders(ctx, &api.GetFolderOptions{
|
||||
|
@ -140,78 +156,120 @@ folders, err := client.GetFolders(ctx, &api.GetFolderOptions{
|
|||
})
|
||||
```
|
||||
|
||||
Getting by ID is also Supported Using the Singular Form:
|
||||
Getting by ID is also supported using the singular form:
|
||||
|
||||
```go
|
||||
resource, err := client.GetResource(ctx, "resource ID")
|
||||
```
|
||||
|
||||
Since the Password is Encrypted (and sometimes the description too) the helper package has a function to decrypt all encrypted fields Automatically:
|
||||
Since the password is encrypted (and sometimes the description too) the helper package has a function to decrypt all encrypted fields automatically:
|
||||
|
||||
```go
|
||||
folderParentID, name, username, uri, password, description, err := helper.GetResource(ctx, client, "resource id")
|
||||
```
|
||||
|
||||
## Updating
|
||||
The Helper Package has a function to save you needing to deal with Resource Types When Updating a Resource:
|
||||
|
||||
The helper package has a function to save you from dealing with resource types when updating a resource:
|
||||
|
||||
```go
|
||||
err := helper.UpdateResource(ctx, client,"resource id", "name", "username", "https://test.example.com", "pass123", "very descriptive")
|
||||
err = helper.UpdateResource(
|
||||
ctx, // Context
|
||||
client, // API Client
|
||||
"id", // Resource ID
|
||||
"name", // Name
|
||||
"username", // Username
|
||||
"url", // URI
|
||||
"strong", // Password
|
||||
"very strong", // Description
|
||||
)
|
||||
```
|
||||
|
||||
Note: As Groups are also Complicated to Update there will be a helper function for them in the future.
|
||||
|
||||
For other less Complicated Updates you can Simply use the Client directly:
|
||||
The same goes for Groups:
|
||||
|
||||
```go
|
||||
client.UpdateUser(ctx, "user id", api.User{
|
||||
Profile: &api.Profile{
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
},
|
||||
})
|
||||
err = helper.UpdateGroup(
|
||||
ctx, // Context
|
||||
client, // API Client
|
||||
"id", // Group ID
|
||||
"name", // Group Name
|
||||
[]helper.GroupMembershipOperation{
|
||||
{
|
||||
UserID: "id", // ID of User to Add/Modify/Delete
|
||||
IsGroupManager: true, // Should User be a Group Manager
|
||||
Delete: false, // Should this User be Remove from the Group
|
||||
},
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
And for Users:
|
||||
|
||||
```go
|
||||
err = helper.UpdateUser(
|
||||
ctx, // Context
|
||||
client, // API Client
|
||||
"id", // User ID
|
||||
"user", // Role (user or admin)
|
||||
"firstname", // FirstName
|
||||
"lastname", // LastName
|
||||
)
|
||||
```
|
||||
|
||||
Note: These helpers will only update fields that are not "".
|
||||
|
||||
Helper update functions also exists for Folders.
|
||||
|
||||
## Sharing
|
||||
As Sharing Resources is very Complicated there are multipe helper Functions. During Sharing you will encounter the permissionType.
|
||||
|
||||
The permissionType can be 1 for Read only, 7 for Can Update, 15 for Owner or -1 if you want to delete Existing Permissions.
|
||||
As sharing resources is very complicated there are multiple helper functions.
|
||||
|
||||
The ShareResourceWithUsersAndGroups function Shares the Resource With all Provided Users and Groups with the Given permissionType.
|
||||
During sharing you will encounter the [permission type](https://github.com/passbolt/passbolt_api/blob/858971516c5e61e1f1be37b007693f0869a70486/src/Model/Entity/Permission.php#L43-L45).
|
||||
|
||||
The `permissionType` can be:
|
||||
|
||||
| Code | Meaning |
|
||||
| ---- | -------------------------- |
|
||||
| `1` | "Read-only" |
|
||||
| `7` | "Can update" |
|
||||
| `15` | "Owner" |
|
||||
| `-1` | Delete existing permission |
|
||||
|
||||
The `ShareResourceWithUsersAndGroups` function shares the resource with all provided users and groups with the given `permissionType`.
|
||||
|
||||
```go
|
||||
err := helper.ShareResourceWithUsersAndGroups(ctx, client, "resource id", []string{"user 1 id"}, []string{"group 1 id"}, 7)
|
||||
```
|
||||
|
||||
Note: Existing Permission of Users and Groups will be adjusted to be of the Provided permissionType.
|
||||
Note: Existing permission of users and groups will be adjusted to be of the provided `permissionType`.
|
||||
|
||||
If you need to do something more Complicated like setting Users/Groups to different Type then you can Use ShareResource directly:
|
||||
If you need to do something more complicated like setting users/groups to different types then you can use `ShareResource` directly:
|
||||
|
||||
```go
|
||||
changes := []helper.ShareOperation{}
|
||||
|
||||
// Make this User Owner
|
||||
// Make this user Owner
|
||||
changes = append(changes, ShareOperation{
|
||||
Type: 15,
|
||||
ARO: "User",
|
||||
AROID: "user 1 id",
|
||||
})
|
||||
|
||||
// Make this User Can Update
|
||||
// Make this user "Can Update"
|
||||
changes = append(changes, ShareOperation{
|
||||
Type: 5,
|
||||
ARO: "User",
|
||||
AROID: "user 2 id",
|
||||
})
|
||||
|
||||
// Delete This Users Current Permission
|
||||
// Delete this users current permission
|
||||
changes = append(changes, ShareOperation{
|
||||
Type: -1,
|
||||
ARO: "User",
|
||||
AROID: "user 3 id",
|
||||
})
|
||||
|
||||
// Make this Group Read Only
|
||||
// Make this group "Read-only"
|
||||
changes = append(changes, ShareOperation{
|
||||
Type: 1,
|
||||
ARO: "Group",
|
||||
|
@ -221,22 +279,71 @@ changes = append(changes, ShareOperation{
|
|||
err := helper.ShareResource(ctx, c, resourceID, changes)
|
||||
```
|
||||
|
||||
Note: These Functions are Also Availabe for Folders (PRO)
|
||||
Note: These functions are also available for folders (PRO)
|
||||
|
||||
## Moveing (PRO)
|
||||
In Passbolt PRO there are Folders, during Creation of Resources and Folders you can Specify in which Folder you want to create the Resource / Folder inside of. But if you want to change which Folder the Resource / Folder is in then you can't use the Update function (it is / was possible to update the parent Folder using the Update function but that breaks things). Instead you use the Move function.
|
||||
```
|
||||
## Moving (PRO)
|
||||
|
||||
In Passbolt PRO there are folders, during the creation of resources and folders you can specify in which folder you want to create the resource/folder inside. But if you want to change which folder the resource/folder is in then you can't use the `Update` function (it is/was possible to update the parent folder using the `Update` function but that breaks things). Instead, you use the `Move` function.
|
||||
|
||||
```go
|
||||
err := client.MoveResource(ctx, "resource id", "parent folder id")
|
||||
```
|
||||
```
|
||||
|
||||
```go
|
||||
err := client.MoveFolder(ctx, "folder id", "parent folder id")
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
You can setup a Account using a Invite Link like this:
|
||||
|
||||
```go
|
||||
// Get the UserID and Token from the Invite Link
|
||||
userID, token, err := ParseInviteUrl(url)
|
||||
|
||||
// Make a Client for Registration
|
||||
rClient, err := api.NewClient(nil, "", "https://localhost", "", "")
|
||||
|
||||
// Complete Account Setup
|
||||
privkey, err := SetupAccount(ctx, rClient, userID, token, "password123")
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
You can Verify that the Server hasen't changed, for that you need to initially setup the Verification and save the returned values. Then you can Verify that the serverkey hasen't changed since you setup the Verification. Note this Only Works if the client is not logged in.
|
||||
|
||||
```go
|
||||
// Setup the Verification
|
||||
token, encToken, err := client.SetupServerVerification(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// You Need to save these
|
||||
fmt.Println("Token: ", token)
|
||||
fmt.Println("enc Token: ", encToken)
|
||||
// Now you can Verify the Server
|
||||
err = client.VerifyServer(ctx, token, encToken)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
## MFA
|
||||
|
||||
go-passbolt now supports MFA! You can set it up using the Client's `MFACallback` function, it will provide everything you need to complete any MFA challenges. When your done you just need to return the new MFA Cookie (usually called passbolt_mfa). The helper package has a example implementation for a noninteractive TOTP Setup under helper/mfa.go in the function `AddMFACallbackTOTP`.
|
||||
|
||||
## Other
|
||||
These Examples are just the main Usecases of this Modules, there are many more API calls that are supported. Look at the [Reference](https://pkg.go.dev/github.com/speatzle/go-passbolt) for more information.
|
||||
|
||||
These examples are just the main use cases of these Modules, many more API calls are supported. Look at the [reference](https://pkg.go.dev/github.com/passbolt/go-passbolt) for more information.
|
||||
|
||||
## Full Example
|
||||
This Example Creates a Resource, Searches for a User Named Test User, Checks that its Not itself and Shares the Password with the Test User if Nessesary:
|
||||
|
||||
This example:
|
||||
|
||||
1. Creates a resource;
|
||||
2. Searches for a user named "Test User";
|
||||
3. Checks that it's not itself; and,
|
||||
4. Shares the password with the "Test User" if necessary:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
@ -245,8 +352,8 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/speatzle/go-passbolt/api"
|
||||
"github.com/speatzle/go-passbolt/helper"
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
"github.com/passbolt/go-passbolt/helper"
|
||||
)
|
||||
|
||||
const address = "https://passbolt.example.com"
|
||||
|
@ -280,7 +387,7 @@ func main() {
|
|||
"user123", // Username
|
||||
"https://test.example.com", // URI
|
||||
"securePassword123", // Password
|
||||
"This is a Account for the example test portal", // Description
|
||||
"This is an Account for the example test portal", // Description
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -301,7 +408,7 @@ func main() {
|
|||
|
||||
if client.GetUserID() == users[0].ID {
|
||||
fmt.Println("I am the Test User, No Need to Share Password With myself")
|
||||
client.Logout(ctx)
|
||||
client.Logout(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -314,9 +421,3 @@ func main() {
|
|||
client.Logout(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
# TODO
|
||||
- get a Passbolt Instance to Work in Github Actions
|
||||
- write Integration Tests
|
||||
- add ability to verify Server
|
||||
- add helper functions for updating Groups
|
||||
|
|
33
api/api.go
33
api/api.go
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// APIResponse is the Struct representation of a Json Response
|
||||
|
@ -32,9 +33,11 @@ func (c *Client) DoCustomRequest(ctx context.Context, method, path, version stri
|
|||
|
||||
// DoCustomRequestAndReturnRawResponse Executes a Custom Request and returns a APIResponse and the Raw HTTP Response
|
||||
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)
|
||||
firstTime := true
|
||||
start:
|
||||
u, err := generateURL(*c.baseURL, path, version, opts)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Adding Request Options: %w", err)
|
||||
return nil, nil, fmt.Errorf("Generating Path: %w", err)
|
||||
}
|
||||
|
||||
req, err := c.newRequest(method, u, body)
|
||||
|
@ -48,9 +51,35 @@ func (c *Client) DoCustomRequestAndReturnRawResponse(ctx context.Context, method
|
|||
return r, &res, fmt.Errorf("Doing Request: %w", err)
|
||||
}
|
||||
|
||||
// Because of MFA i need to do the csrf token stuff here
|
||||
if c.csrfToken.Name == "" {
|
||||
for _, cookie := range r.Cookies() {
|
||||
if cookie.Name == "csrfToken" {
|
||||
c.csrfToken = *cookie
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if res.Header.Status == "success" {
|
||||
return r, &res, nil
|
||||
} else if res.Header.Status == "error" {
|
||||
if res.Header.Code == 403 && strings.HasSuffix(res.Header.URL, "/mfa/verify/error.json") {
|
||||
if !firstTime {
|
||||
// if we are here this probably means that the MFA callback is broken, to prevent a infinite loop lets error here
|
||||
return r, &res, fmt.Errorf("Got MFA challenge twice in a row, is your MFA Callback broken? Bailing to prevent loop...:")
|
||||
}
|
||||
if c.MFACallback != nil {
|
||||
c.mfaToken, err = c.MFACallback(ctx, c, &res)
|
||||
if err != nil {
|
||||
return r, &res, fmt.Errorf("MFA Callback: %w", err)
|
||||
}
|
||||
// ok, we got the MFA challenge and the callback presumably handled it so we can retry the original request
|
||||
firstTime = false
|
||||
goto start
|
||||
} else {
|
||||
return r, &res, fmt.Errorf("Got MFA Challenge but the MFA callback is not defined")
|
||||
}
|
||||
}
|
||||
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))
|
||||
|
|
85
api/auth.go
85
api/auth.go
|
@ -6,19 +6,12 @@ import (
|
|||
"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"`
|
||||
|
@ -30,24 +23,6 @@ type GPGAuth struct {
|
|||
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)
|
||||
|
@ -56,6 +31,11 @@ func (c *Client) CheckSession(ctx context.Context) bool {
|
|||
|
||||
// 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 {
|
||||
c.csrfToken = http.Cookie{}
|
||||
|
||||
if c.userPrivateKey == "" {
|
||||
return fmt.Errorf("Client has no Private Key")
|
||||
}
|
||||
|
||||
privateKeyObj, err := crypto.NewKeyFromArmored(c.userPrivateKey)
|
||||
if err != nil {
|
||||
|
@ -109,31 +89,21 @@ func (c *Client) Login(ctx context.Context) error {
|
|||
// Session Cookie in older Passbolt Versions
|
||||
} else if cookie.Name == "CAKEPHP" {
|
||||
c.sessionToken = *cookie
|
||||
// Session Cookie in Cloud version?
|
||||
} else if cookie.Name == "PHPSESSID" {
|
||||
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)
|
||||
// Because of MFA, the custom Request Function now Fetches the CSRF token, we still need the user for his public key
|
||||
apiMsg, err := c.DoCustomRequest(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)
|
||||
|
@ -142,10 +112,7 @@ func (c *Client) Login(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
randomString := randStringBytesRmndr(50)
|
||||
armor, err := helper.EncryptMessageArmored(user.GPGKey.ArmoredKey, randomString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Encryping PublicKey Validation Message: %w", err)
|
||||
|
@ -175,33 +142,3 @@ func (c *Client) Logout(ctx context.Context) error {
|
|||
c.csrfToken = http.Cookie{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserID Gets the ID of the Current User
|
||||
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
|
||||
}
|
||||
|
|
111
api/client.go
111
api/client.go
|
@ -9,6 +9,7 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/google/go-querystring/query"
|
||||
|
@ -22,6 +23,7 @@ type Client struct {
|
|||
|
||||
sessionToken http.Cookie
|
||||
csrfToken http.Cookie
|
||||
mfaToken http.Cookie
|
||||
|
||||
// for some reason []byte is used for Passwords in gopenpgp instead of string like they do for keys...
|
||||
userPassword []byte
|
||||
|
@ -29,13 +31,26 @@ type Client struct {
|
|||
userPublicKey string
|
||||
userID string
|
||||
|
||||
// used for solving MFA challenges. You can block this to for example wait for user input.
|
||||
// You shouden't run any unrelated API Calls while you are in this callback.
|
||||
// You need to Return the Cookie that Passbolt expects to verify you MFA, usually it is called passbolt_mfa
|
||||
MFACallback func(ctx context.Context, c *Client, res *APIResponse) (http.Cookie, error)
|
||||
|
||||
// Enable Debug Logging
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// PublicKeyReponse the Body of a Public Key Api Request
|
||||
type PublicKeyReponse struct {
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Keydata string `json:"keydata"`
|
||||
}
|
||||
|
||||
// NewClient Returns a new Passbolt Client.
|
||||
// if httpClient is nil http.DefaultClient will be used.
|
||||
// if UserAgent is "" "goPassboltClient/1.0" will be used.
|
||||
// if UserPrivateKey is "" Key Setup is Skipped to Enable using the Client for User Registration, Most other function will be broken.
|
||||
// After Registration a new Client Should be Created.
|
||||
func NewClient(httpClient *http.Client, UserAgent, BaseURL, UserPrivateKey, UserPassword string) (*Client, error) {
|
||||
if httpClient == nil {
|
||||
httpClient = http.DefaultClient
|
||||
|
@ -49,22 +64,24 @@ func NewClient(httpClient *http.Client, UserAgent, BaseURL, UserPrivateKey, User
|
|||
return nil, fmt.Errorf("Parsing Base URL: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// Verify that the Given Privatekey and Password are valid and work Together if we were provieded one
|
||||
if UserPrivateKey != "" {
|
||||
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()
|
||||
// Cleanup Secrets
|
||||
privateKeyRing.ClearPrivateParams()
|
||||
}
|
||||
|
||||
// Create Client Object
|
||||
c := &Client{
|
||||
|
@ -77,12 +94,7 @@ func NewClient(httpClient *http.Client, UserAgent, BaseURL, UserPrivateKey, User
|
|||
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)
|
||||
func (c *Client) newRequest(method, url string, body interface{}) (*http.Request, error) {
|
||||
var buf io.ReadWriter
|
||||
if body != nil {
|
||||
buf = new(bytes.Buffer)
|
||||
|
@ -91,7 +103,8 @@ func (c *Client) newRequest(method, path string, body interface{}) (*http.Reques
|
|||
return nil, fmt.Errorf("JSON Encoding Request: %w", err)
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(method, u.String(), buf)
|
||||
|
||||
req, err := http.NewRequest(method, url, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating HTTP Request: %w", err)
|
||||
}
|
||||
|
@ -103,6 +116,18 @@ func (c *Client) newRequest(method, path string, body interface{}) (*http.Reques
|
|||
req.Header.Set("X-CSRF-Token", c.csrfToken.Value)
|
||||
req.AddCookie(&c.sessionToken)
|
||||
req.AddCookie(&c.csrfToken)
|
||||
if c.mfaToken.Name != "" {
|
||||
req.AddCookie(&c.mfaToken)
|
||||
}
|
||||
|
||||
// Debugging
|
||||
c.log("Request URL: %v", req.URL.String())
|
||||
if c.Debug && body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err == nil {
|
||||
c.log("Raw Request: %v", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
@ -127,10 +152,13 @@ func (c *Client) do(ctx context.Context, req *http.Request, v *APIResponse) (*ht
|
|||
return resp, fmt.Errorf("Error Reading Resopnse Body: %w", err)
|
||||
}
|
||||
|
||||
c.log("Raw Response: %v", string(bodyBytes))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -141,19 +169,42 @@ func (c *Client) log(msg string, args ...interface{}) {
|
|||
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)
|
||||
}
|
||||
func generateURL(base url.URL, p, version string, opt interface{}) (string, error) {
|
||||
base.Path = path.Join(base.Path, p)
|
||||
|
||||
vs, err := query.Values(opt)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("Getting URL Query Values: %w", err)
|
||||
return "", fmt.Errorf("Getting URL Query Values: %w", err)
|
||||
}
|
||||
if version != "" {
|
||||
vs.Add("api-version", version)
|
||||
}
|
||||
u.RawQuery = vs.Encode()
|
||||
return u.String(), nil
|
||||
base.RawQuery = vs.Encode()
|
||||
return base.String(), nil
|
||||
}
|
||||
|
||||
// GetUserID Gets the ID of the Current User
|
||||
func (c *Client) GetUserID() string {
|
||||
return c.userID
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Lets get the actual Fingerprint instead of trusting the Server
|
||||
privateKeyObj, err := crypto.NewKeyFromArmored(c.userPrivateKey)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Parsing Server Key: %w", err)
|
||||
}
|
||||
return body.Keydata, privateKeyObj.GetFingerprint(), nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Comment is a Comment
|
||||
|
@ -29,6 +30,10 @@ type GetCommentsOptions struct {
|
|||
|
||||
// GetComments gets all Passbolt Comments an The Specified Resource
|
||||
func (c *Client) GetComments(ctx context.Context, resourceID string, opts *GetCommentsOptions) ([]Comment, error) {
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/comments/resource/"+resourceID+".json", "v2", nil, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -44,6 +49,10 @@ func (c *Client) GetComments(ctx context.Context, resourceID string, opts *GetCo
|
|||
|
||||
// CreateComment Creates a new Passbolt Comment
|
||||
func (c *Client) CreateComment(ctx context.Context, resourceID string, comment Comment) (*Comment, error) {
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "POST", "/comments/resource/"+resourceID+".json", "v2", comment, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -58,6 +67,10 @@ func (c *Client) CreateComment(ctx context.Context, resourceID string, comment C
|
|||
|
||||
// UpdateComment Updates a existing Passbolt Comment
|
||||
func (c *Client) UpdateComment(ctx context.Context, commentID string, comment Comment) (*Comment, error) {
|
||||
err := checkUUIDFormat(commentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "PUT", "/comments/"+commentID+".json", "v2", comment, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -72,7 +85,11 @@ func (c *Client) UpdateComment(ctx context.Context, commentID string, comment Co
|
|||
|
||||
// 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)
|
||||
err := checkUUIDFormat(commentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "DELETE", "/comments/"+commentID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,19 +1,34 @@
|
|||
package api
|
||||
|
||||
import "github.com/ProtonMail/gopenpgp/v2/helper"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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) {
|
||||
if c.userPrivateKey == "" {
|
||||
return "", fmt.Errorf("Client has no Private Key")
|
||||
} else if c.userPublicKey == "" {
|
||||
return "", fmt.Errorf("Client has no Public Key")
|
||||
}
|
||||
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) {
|
||||
if c.userPrivateKey == "" {
|
||||
return "", fmt.Errorf("Client has no Private Key")
|
||||
}
|
||||
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
|
||||
// DecryptMessage decrypts a message using the users Private Key
|
||||
func (c *Client) DecryptMessage(message string) (string, error) {
|
||||
if c.userPrivateKey == "" {
|
||||
return "", fmt.Errorf("Client has no Private Key")
|
||||
}
|
||||
// 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)
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Favorite is a Favorite
|
||||
|
@ -16,6 +17,10 @@ type Favorite struct {
|
|||
|
||||
// CreateFavorite Creates a new Passbolt Favorite for the given Resource ID
|
||||
func (c *Client) CreateFavorite(ctx context.Context, resourceID string) (*Favorite, error) {
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "POST", "/favorites/resource/"+resourceID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -31,7 +36,11 @@ func (c *Client) CreateFavorite(ctx context.Context, resourceID string) (*Favori
|
|||
|
||||
// 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)
|
||||
err := checkUUIDFormat(favoriteID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "DELETE", "/favorites/"+favoriteID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Folder is a Folder
|
||||
|
@ -20,6 +21,24 @@ type Folder struct {
|
|||
ChildrenFolders []Folder `json:"children_folders,omitempty"`
|
||||
}
|
||||
|
||||
// GetFoldersOptions are all available query parameters
|
||||
type GetFoldersOptions 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"`
|
||||
}
|
||||
|
||||
// GetFolderOptions are all available query parameters
|
||||
type GetFolderOptions struct {
|
||||
ContainChildrenResources bool `url:"contain[children_resources],omitempty"`
|
||||
|
@ -32,14 +51,10 @@ type GetFolderOptions struct {
|
|||
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) {
|
||||
func (c *Client) GetFolders(ctx context.Context, opts *GetFoldersOptions) ([]Folder, error) {
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/folders.json", "v2", nil, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -68,8 +83,12 @@ func (c *Client) CreateFolder(ctx context.Context, folder Folder) (*Folder, erro
|
|||
}
|
||||
|
||||
// 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)
|
||||
func (c *Client) GetFolder(ctx context.Context, folderID string, opts *GetFolderOptions) (*Folder, error) {
|
||||
err := checkUUIDFormat(folderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/folders/"+folderID+".json", "v2", nil, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -84,6 +103,10 @@ func (c *Client) GetFolder(ctx context.Context, folderID string) (*Folder, error
|
|||
|
||||
// UpdateFolder Updates a existing Passbolt Folder
|
||||
func (c *Client) UpdateFolder(ctx context.Context, folderID string, folder Folder) (*Folder, error) {
|
||||
err := checkUUIDFormat(folderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "PUT", "/folders/"+folderID+".json", "v2", folder, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -98,7 +121,11 @@ func (c *Client) UpdateFolder(ctx context.Context, folderID string, folder Folde
|
|||
|
||||
// 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)
|
||||
err := checkUUIDFormat(folderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "DELETE", "/folders/"+folderID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -107,7 +134,11 @@ func (c *Client) DeleteFolder(ctx context.Context, folderID string) error {
|
|||
|
||||
// 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{
|
||||
err := checkUUIDFormat(folderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "PUT", "/move/folder/"+folderID+".json", "v2", Folder{
|
||||
FolderParentID: folderParentID,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GPGKey is a GPGKey
|
||||
|
@ -43,6 +44,10 @@ func (c *Client) GetGPGKeys(ctx context.Context, opts *GetGPGKeysOptions) ([]GPG
|
|||
|
||||
// GetGPGKey gets a Passbolt GPGKey
|
||||
func (c *Client) GetGPGKey(ctx context.Context, gpgkeyID string) (*GPGKey, error) {
|
||||
err := checkUUIDFormat(gpgkeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/gpgkeys/"+gpgkeyID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
115
api/groups.go
115
api/groups.go
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//Group is a Group
|
||||
|
@ -14,7 +15,39 @@ type Group struct {
|
|||
Deleted bool `json:"deleted,omitempty"`
|
||||
Modified *Time `json:"modified,omitempty"`
|
||||
ModifiedBy string `json:"modified_by,omitempty"`
|
||||
GroupUsers []User `json:"groups_users,omitempty"`
|
||||
// This does not Contain Profile for Users Anymore...
|
||||
GroupUsers []GroupMembership `json:"groups_users,omitempty"`
|
||||
// This is new and undocumented but as all the data
|
||||
Users []GroupUser `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
type GroupUser struct {
|
||||
User
|
||||
JoinData GroupJoinData `json:"_join_data,omitempty"`
|
||||
}
|
||||
|
||||
type GroupJoinData struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
IsAdmin bool `json:"is_admin,omitempty"`
|
||||
Created *Time `json:"created,omitempty"`
|
||||
}
|
||||
|
||||
type GroupMembership struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
IsAdmin bool `json:"is_admin,omitempty"`
|
||||
Delete bool `json:"delete,omitempty"`
|
||||
User User `json:"user,omitempty"`
|
||||
Created *Time `json:"created,omitempty"`
|
||||
}
|
||||
|
||||
type GroupUpdate struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
GroupChanges []GroupMembership `json:"groups_users,omitempty"`
|
||||
Secrets []Secret `json:"secrets,omitempty"`
|
||||
}
|
||||
|
||||
// GetGroupsOptions are all available query parameters
|
||||
|
@ -22,11 +55,43 @@ 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"`
|
||||
ContainModifier bool `url:"contain[modifier],omitempty"`
|
||||
ContainModifierProfile bool `url:"contain[modifier.profile],omitempty"`
|
||||
ContainMyGroupUser bool `url:"contain[my_group_user],omitempty"`
|
||||
ContainUsers bool `url:"contain[users],omitempty"`
|
||||
ContainGroupsUsers bool `url:"contain[groups_users],omitempty"`
|
||||
ContainGroupsUsersUser bool `url:"contain[groups_users.user],omitempty"`
|
||||
ContainGroupsUsersUserProfile bool `url:"contain[groups_users.user.profile],omitempty"`
|
||||
ContainGroupsUsersUserGPGKey bool `url:"contain[groups_users.user.gpgkey],omitempty"`
|
||||
}
|
||||
|
||||
// UpdateGroupDryRunResult is the Result of a Update Group DryRun
|
||||
type UpdateGroupDryRunResult struct {
|
||||
DryRun UpdateGroupDryRun `json:"dry-run,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateGroupDryRun contains the Actual Secrets Needed to update the group
|
||||
type UpdateGroupDryRun struct {
|
||||
// for which users the secrets need to be reencrypted
|
||||
SecretsNeeded []UpdateGroupSecretsNeededContainer `json:"SecretsNeeded,omitempty"`
|
||||
// secrets needed to be reencrypted
|
||||
Secrets []GroupSecret `json:"Secrets,omitempty"`
|
||||
}
|
||||
|
||||
// GroupSecret is a unnessesary container...
|
||||
type GroupSecret struct {
|
||||
Secret []Secret `json:"secret,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateGroupSecretsNeededContainer is a unnessesary container...
|
||||
type UpdateGroupSecretsNeededContainer struct {
|
||||
Secret UpdateGroupDryRunSecretsNeeded `json:"Secret,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateGroupDryRunSecretsNeeded a secret that needs to be reencrypted for a specific user
|
||||
type UpdateGroupDryRunSecretsNeeded struct {
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
// GetGroups gets all Passbolt Groups
|
||||
|
@ -60,6 +125,10 @@ func (c *Client) CreateGroup(ctx context.Context, group Group) (*Group, error) {
|
|||
|
||||
// GetGroup gets a Passbolt Group
|
||||
func (c *Client) GetGroup(ctx context.Context, groupID string) (*Group, error) {
|
||||
err := checkUUIDFormat(groupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/groups/"+groupID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -74,12 +143,16 @@ func (c *Client) GetGroup(ctx context.Context, groupID string) (*Group, error) {
|
|||
}
|
||||
|
||||
// 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)
|
||||
func (c *Client) UpdateGroup(ctx context.Context, groupID string, update GroupUpdate) (*Group, error) {
|
||||
err := checkUUIDFormat(groupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "PUT", "/groups/"+groupID+".json", "v2", update, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var group Group
|
||||
err = json.Unmarshal(msg.Body, &group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -87,9 +160,31 @@ func (c *Client) UpdateGroup(ctx context.Context, groupID string, group Group) (
|
|||
return &group, nil
|
||||
}
|
||||
|
||||
// UpdateGroupDryRun Checks that a Passbolt Group update passes validation
|
||||
func (c *Client) UpdateGroupDryRun(ctx context.Context, groupID string, update GroupUpdate) (*UpdateGroupDryRunResult, error) {
|
||||
err := checkUUIDFormat(groupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "PUT", "/groups/"+groupID+"/dry-run.json", "v2", update, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result UpdateGroupDryRunResult
|
||||
err = json.Unmarshal(msg.Body, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, 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)
|
||||
err := checkUUIDFormat(groupID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "DELETE", "/groups/"+groupID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
13
api/mfa.go
Normal file
13
api/mfa.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package api
|
||||
|
||||
type MFAChallenge struct {
|
||||
Provider MFAProviders `json:"providers,omitempty"`
|
||||
}
|
||||
|
||||
type MFAProviders struct {
|
||||
TOTP string `json:"totp,omitempty"`
|
||||
}
|
||||
|
||||
type MFAChallengeResponse struct {
|
||||
TOTP string `json:"totp,omitempty"`
|
||||
}
|
64
api/misc.go
64
api/misc.go
|
@ -1,25 +1,53 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
var isUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||
|
||||
func randStringBytesRmndr(length int) string {
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func checkUUIDFormat(data string) error {
|
||||
if !isUUID.MatchString(data) {
|
||||
return fmt.Errorf("UUID is not in the valid format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Permission is a Permission
|
||||
|
@ -21,6 +22,10 @@ type Permission struct {
|
|||
|
||||
// GetResourcePermissions gets a Resources Permissions
|
||||
func (c *Client) GetResourcePermissions(ctx context.Context, resourceID string) ([]Permission, error) {
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/permissions/resource/"+resourceID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -33,18 +38,3 @@ func (c *Client) GetResourcePermissions(ctx context.Context, resourceID string)
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -3,9 +3,10 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//ResourceType is the Type of a Resource
|
||||
// ResourceType is the Type of a Resource
|
||||
type ResourceType struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
|
@ -15,6 +16,11 @@ type ResourceType struct {
|
|||
Modified *Time `json:"modified,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceTypeSchema struct {
|
||||
Resource json.RawMessage `json:"resource"`
|
||||
Secret json.RawMessage `json:"secret"`
|
||||
}
|
||||
|
||||
// GetResourceTypesOptions is a placeholder for future options
|
||||
type GetResourceTypesOptions struct {
|
||||
}
|
||||
|
@ -36,6 +42,10 @@ func (c *Client) GetResourceTypes(ctx context.Context, opts *GetResourceTypesOpt
|
|||
|
||||
// GetResourceType gets a Passbolt Type
|
||||
func (c *Client) GetResourceType(ctx context.Context, typeID string) (*ResourceType, error) {
|
||||
err := checkUUIDFormat(typeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/resource-types/"+typeID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -3,30 +3,32 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// for now the only Field like that is the Description.
|
||||
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"`
|
||||
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"`
|
||||
ResourceType ResourceType `json:"resource_type,omitempty"`
|
||||
Secrets []Secret `json:"secrets,omitempty"`
|
||||
Tags []Tag `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// Tag is a Passbolt Password Tag
|
||||
|
@ -38,21 +40,23 @@ type Tag struct {
|
|||
|
||||
// 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"`
|
||||
FilterIsFavorite bool `url:"filter[is-favorite],omitempty"`
|
||||
FilterIsSharedWithGroup string `url:"filter[is-shared-with-group],omitempty"`
|
||||
FilterIsOwnedByMe bool `url:"filter[is-owned-by-me],omitempty"`
|
||||
FilterIsSharedWithMe bool `url:"filter[is-shared-with-me],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"`
|
||||
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"`
|
||||
ContainSecret bool `url:"contain[secret],omitempty"`
|
||||
ContainResourceType bool `url:"contain[resource-type],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"`
|
||||
}
|
||||
|
||||
|
@ -87,6 +91,10 @@ func (c *Client) CreateResource(ctx context.Context, resource Resource) (*Resour
|
|||
|
||||
// GetResource gets a Passbolt Resource
|
||||
func (c *Client) GetResource(ctx context.Context, resourceID string) (*Resource, error) {
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/resources/"+resourceID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -102,6 +110,10 @@ func (c *Client) GetResource(ctx context.Context, resourceID string) (*Resource,
|
|||
|
||||
// UpdateResource Updates a existing Passbolt Resource
|
||||
func (c *Client) UpdateResource(ctx context.Context, resourceID string, resource Resource) (*Resource, error) {
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "PUT", "/resources/"+resourceID+".json", "v2", resource, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -116,7 +128,11 @@ func (c *Client) UpdateResource(ctx context.Context, resourceID string, resource
|
|||
|
||||
// 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)
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "DELETE", "/resources/"+resourceID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -125,7 +141,11 @@ func (c *Client) DeleteResource(ctx context.Context, resourceID string) error {
|
|||
|
||||
// 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{
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "PUT", "/move/resource/"+resourceID+".json", "v2", Resource{
|
||||
FolderParentID: folderParentID,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Secret is a Secret
|
||||
|
@ -21,8 +22,31 @@ type SecretDataTypePasswordAndDescription struct {
|
|||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type SecretDataTOTP struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Digits int `json:"digits"`
|
||||
Period int `json:"period"`
|
||||
}
|
||||
|
||||
// SecretDataTypeTOTP is the format a secret of resource type "totp" is stored in
|
||||
type SecretDataTypeTOTP struct {
|
||||
TOTP SecretDataTOTP `json:"totp"`
|
||||
}
|
||||
|
||||
// SecretDataTypePasswordDescriptionTOTP is the format a secret of resource type "password-description-totp" is stored in
|
||||
type SecretDataTypePasswordDescriptionTOTP struct {
|
||||
Password string `json:"password"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TOTP SecretDataTOTP `json:"totp"`
|
||||
}
|
||||
|
||||
// GetSecret gets a Passbolt Secret
|
||||
func (c *Client) GetSecret(ctx context.Context, resourceID string) (*Secret, error) {
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/secrets/resource/"+resourceID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
57
api/setup.go
Normal file
57
api/setup.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type SetupInstallResponse struct {
|
||||
User `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type AuthenticationToken struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
type SetupCompleteRequest struct {
|
||||
AuthenticationToken AuthenticationToken `json:"authenticationtoken,omitempty"`
|
||||
GPGKey GPGKey `json:"gpgkey,omitempty"`
|
||||
User User `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// SetupInstall validates the userid and token used for Account setup, gives back the User Information
|
||||
func (c *Client) SetupInstall(ctx context.Context, userID, token string) (*SetupInstallResponse, error) {
|
||||
err := checkUUIDFormat(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
err = checkUUIDFormat(token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking Token format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/setup/install/"+userID+"/"+token+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var install SetupInstallResponse
|
||||
err = json.Unmarshal(msg.Body, &install)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &install, nil
|
||||
}
|
||||
|
||||
// SetupComplete Completes setup of a Passbolt Account
|
||||
func (c *Client) SetupComplete(ctx context.Context, userID string, request SetupCompleteRequest) error {
|
||||
err := checkUUIDFormat(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "POST", "/setup/complete/"+userID+".json", "v2", request, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
17
api/share.go
17
api/share.go
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ResourceShareRequest is a ResourceShareRequest
|
||||
|
@ -61,7 +62,11 @@ func (c *Client) SearchAROs(ctx context.Context, opts SearchAROsOptions) ([]ARO,
|
|||
|
||||
// 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)
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "PUT", "/share/resource/"+resourceID+".json", "v2", shareRequest, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -71,8 +76,12 @@ func (c *Client) ShareResource(ctx context.Context, resourceID string, shareRequ
|
|||
|
||||
// ShareFolder Shares a Folder with AROs
|
||||
func (c *Client) ShareFolder(ctx context.Context, folderID string, permissions []Permission) error {
|
||||
err := checkUUIDFormat(folderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
f := Folder{Permissions: permissions}
|
||||
_, err := c.DoCustomRequest(ctx, "PUT", "/share/folder/"+folderID+".json", "v2", f, nil)
|
||||
_, err = c.DoCustomRequest(ctx, "PUT", "/share/folder/"+folderID+".json", "v2", f, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -82,6 +91,10 @@ func (c *Client) ShareFolder(ctx context.Context, folderID string, permissions [
|
|||
|
||||
// SimulateShareResource Simulates Shareing a Resource with AROs
|
||||
func (c *Client) SimulateShareResource(ctx context.Context, resourceID string, shareRequest ResourceShareRequest) (*ResourceShareSimulationResult, error) {
|
||||
err := checkUUIDFormat(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "POST", "/share/simulate/resource/"+resourceID+".json", "v2", shareRequest, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
32
api/users.go
32
api/users.go
|
@ -3,8 +3,11 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const UserLocaleENUK = "en-UK"
|
||||
|
||||
// User contains information about a passbolt User
|
||||
type User struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
|
@ -20,6 +23,7 @@ type User struct {
|
|||
Role *Role `json:"role,omitempty"`
|
||||
GPGKey *GPGKey `json:"gpgKey,omitempty"`
|
||||
LastLoggedIn string `json:"last_logged_in,omitempty"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
}
|
||||
|
||||
// Profile is a Profile
|
||||
|
@ -34,10 +38,10 @@ type Profile struct {
|
|||
|
||||
// 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"`
|
||||
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"`
|
||||
}
|
||||
|
@ -78,6 +82,10 @@ func (c *Client) GetMe(ctx context.Context) (*User, error) {
|
|||
|
||||
// GetUser gets a Passbolt User
|
||||
func (c *Client) GetUser(ctx context.Context, userID string) (*User, error) {
|
||||
err := checkUUIDFormat(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "GET", "/users/"+userID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -93,6 +101,10 @@ func (c *Client) GetUser(ctx context.Context, userID string) (*User, error) {
|
|||
|
||||
// UpdateUser Updates a existing Passbolt User
|
||||
func (c *Client) UpdateUser(ctx context.Context, userID string, user User) (*User, error) {
|
||||
err := checkUUIDFormat(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
msg, err := c.DoCustomRequest(ctx, "PUT", "/users/"+userID+".json", "v2", user, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -107,7 +119,11 @@ func (c *Client) UpdateUser(ctx context.Context, userID string, user User) (*Use
|
|||
|
||||
// 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)
|
||||
err := checkUUIDFormat(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "DELETE", "/users/"+userID+".json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -116,7 +132,11 @@ func (c *Client) DeleteUser(ctx context.Context, userID string) error {
|
|||
|
||||
// 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)
|
||||
err := checkUUIDFormat(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Checking ID format: %w", err)
|
||||
}
|
||||
_, err = c.DoCustomRequest(ctx, "DELETE", "/users/"+userID+"/dry-run.json", "v2", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
67
api/verify.go
Normal file
67
api/verify.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// GPGVerifyContainer is used for verification
|
||||
type GPGVerifyContainer struct {
|
||||
Req GPGVerify `json:"gpg_auth"`
|
||||
}
|
||||
|
||||
// GPGVerify is used for verification
|
||||
type GPGVerify struct {
|
||||
KeyID string `json:"keyid"`
|
||||
Token string `json:"server_verify_token,omitempty"`
|
||||
}
|
||||
|
||||
// SetupServerVerification sets up Server Verification, Only works before login
|
||||
func (c *Client) SetupServerVerification(ctx context.Context) (string, string, error) {
|
||||
serverKey, _, err := c.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Getting Server Key: %w", err)
|
||||
}
|
||||
uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Generating UUID: %w", err)
|
||||
}
|
||||
token := "gpgauthv1.3.0|36|" + uuid.String() + "|gpgauthv1.3.0"
|
||||
encToken, err := c.EncryptMessageWithPublicKey(serverKey, token)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Encrypting Challenge: %w", err)
|
||||
}
|
||||
err = c.VerifyServer(ctx, token, encToken)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Initial Verification: %w", err)
|
||||
}
|
||||
return token, encToken, err
|
||||
}
|
||||
|
||||
// VerifyServer verifys that the Server is still the same one as during the Setup, Only works before login
|
||||
func (c *Client) VerifyServer(ctx context.Context, token, encToken string) error {
|
||||
privateKeyObj, err := crypto.NewKeyFromArmored(c.userPrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Parsing User Private Key: %w", err)
|
||||
}
|
||||
|
||||
data := GPGVerifyContainer{
|
||||
Req: GPGVerify{
|
||||
Token: encToken,
|
||||
KeyID: privateKeyObj.GetFingerprint(),
|
||||
},
|
||||
}
|
||||
raw, _, err := c.DoCustomRequestAndReturnRawResponse(ctx, "POST", "/auth/verify.json", "v2", data, nil)
|
||||
if err != nil && !strings.Contains(err.Error(), "The authentication failed.") {
|
||||
return fmt.Errorf("Sending Verification Challenge: %w", err)
|
||||
}
|
||||
|
||||
if raw.Header.Get("X-GPGAuth-Verify-Response") != token {
|
||||
return fmt.Errorf("Server Response did not Match Saved Token")
|
||||
}
|
||||
return nil
|
||||
}
|
23
go.mod
23
go.mod
|
@ -1,13 +1,20 @@
|
|||
module github.com/speatzle/go-passbolt
|
||||
module github.com/passbolt/go-passbolt
|
||||
|
||||
go 1.16
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c // indirect
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.2.2
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.3
|
||||
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
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||
github.com/cloudflare/circl v1.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
|
108
go.sum
108
go.sum
|
@ -1,11 +1,21 @@
|
|||
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/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.5 h1:STOY3vgES59gNgoOt2w0nyHBjKViB/qSg7NjbQWPJkA=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.3 h1:1jHlELwCR00qovx2B50DkL/FjYwt/P91RnlsqeOp2Hs=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.3/go.mod h1:LiuOTbnJit8w9ZzOoLscj0kmdALY7hfoCVh5Qlb0bcg=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
|
||||
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
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=
|
||||
|
@ -13,59 +23,71 @@ 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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
|
||||
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=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
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/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/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/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
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/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
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/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -2,8 +2,9 @@ package helper
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/speatzle/go-passbolt/api"
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
)
|
||||
|
||||
// CreateFolder Creates a new Folder
|
||||
|
@ -12,27 +13,44 @@ func CreateFolder(ctx context.Context, c *api.Client, folderParentID, name strin
|
|||
Name: name,
|
||||
FolderParentID: folderParentID,
|
||||
})
|
||||
return f.ID, err
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Creating Folder: %w", err)
|
||||
}
|
||||
return f.ID, nil
|
||||
}
|
||||
|
||||
// GetFolder Gets a Folder
|
||||
func GetFolder(ctx context.Context, c *api.Client, folderID string) (string, string, error) {
|
||||
f, err := c.GetFolder(ctx, folderID)
|
||||
return f.FolderParentID, f.Name, err
|
||||
f, err := c.GetFolder(ctx, folderID, nil)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Getting Folder: %w", err)
|
||||
}
|
||||
return f.FolderParentID, f.Name, nil
|
||||
}
|
||||
|
||||
// UpdateFolder Updates a Folder
|
||||
func UpdateFolder(ctx context.Context, c *api.Client, folderID, name string) error {
|
||||
_, err := c.UpdateFolder(ctx, folderID, api.Folder{Name: name})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Updating Folder: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteFolder Deletes a Folder
|
||||
func DeleteFolder(ctx context.Context, c *api.Client, folderID string) error {
|
||||
return c.DeleteFolder(ctx, folderID)
|
||||
err := c.DeleteFolder(ctx, folderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Deleting Folder: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveFolder Moves a Folder into a Folder
|
||||
func MoveFolder(ctx context.Context, c *api.Client, folderID, folderParentID string) error {
|
||||
return c.MoveFolder(ctx, folderID, folderParentID)
|
||||
err := c.MoveFolder(ctx, folderID, folderParentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Moving Folder: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
204
helper/group.go
Normal file
204
helper/group.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
)
|
||||
|
||||
// GroupMembershipOperation creates/modifies/deletes a group membership
|
||||
type GroupMembershipOperation struct {
|
||||
UserID string
|
||||
IsGroupManager bool
|
||||
Delete bool
|
||||
}
|
||||
|
||||
// GroupMembership contains who and what kind of membership they have with a group
|
||||
type GroupMembership struct {
|
||||
UserID string
|
||||
Username string
|
||||
UserFirstName string
|
||||
UserLastName string
|
||||
IsGroupManager bool
|
||||
}
|
||||
|
||||
// CreateGroup creates a Groups with Name and Memberships
|
||||
func CreateGroup(ctx context.Context, c *api.Client, name string, operations []GroupMembershipOperation) (string, error) {
|
||||
memberships := []api.GroupMembership{}
|
||||
for _, o := range operations {
|
||||
if o.Delete {
|
||||
return "", fmt.Errorf("Cannot Delete Membership during Group Creation")
|
||||
}
|
||||
memberships = append(memberships, api.GroupMembership{
|
||||
UserID: o.UserID,
|
||||
IsAdmin: o.IsGroupManager,
|
||||
})
|
||||
}
|
||||
group, err := c.CreateGroup(ctx, api.Group{
|
||||
Name: name,
|
||||
GroupUsers: memberships,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Creating Group: %w", err)
|
||||
}
|
||||
return group.ID, nil
|
||||
}
|
||||
|
||||
// GetGroup gets a Groups Name and Memberships
|
||||
func GetGroup(ctx context.Context, c *api.Client, groupID string) (string, []GroupMembership, error) {
|
||||
// for some reason the groups index api call does not give back the groups_users even though it is supposed to, so i have to do this...
|
||||
groups, err := c.GetGroups(ctx, &api.GetGroupsOptions{
|
||||
ContainGroupsUsers: true,
|
||||
ContainGroupsUsersUser: true,
|
||||
ContainGroupsUsersUserProfile: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Getting Groups: %w", err)
|
||||
}
|
||||
|
||||
for _, g := range groups {
|
||||
if g.ID == groupID {
|
||||
memberships := []GroupMembership{}
|
||||
for _, m := range g.GroupUsers {
|
||||
memberships = append(memberships, GroupMembership{
|
||||
UserID: m.UserID,
|
||||
Username: m.User.Username,
|
||||
UserFirstName: m.User.Profile.FirstName,
|
||||
UserLastName: m.User.Profile.LastName,
|
||||
IsGroupManager: m.IsAdmin,
|
||||
})
|
||||
}
|
||||
return g.Name, memberships, nil
|
||||
}
|
||||
}
|
||||
return "", nil, fmt.Errorf("Cannot Find Group in API Response")
|
||||
}
|
||||
|
||||
// UpdateGroup Updates a Groups Name and Memberships
|
||||
func UpdateGroup(ctx context.Context, c *api.Client, groupID, name string, operations []GroupMembershipOperation) error {
|
||||
// for some reason the groups index api call does not give back the groups_users even though it is supposed to, so i have to do this...
|
||||
groups, err := c.GetGroups(ctx, &api.GetGroupsOptions{
|
||||
ContainGroupsUsers: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Getting Groups: %w", err)
|
||||
}
|
||||
|
||||
var currentMemberships []api.GroupMembership
|
||||
var currentName string
|
||||
for _, g := range groups {
|
||||
if g.ID == groupID {
|
||||
currentMemberships = g.GroupUsers
|
||||
currentName = g.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if currentMemberships == nil {
|
||||
return fmt.Errorf("Cannot Find Group with ID %v", groupID)
|
||||
}
|
||||
|
||||
request := api.GroupUpdate{
|
||||
Name: name,
|
||||
GroupChanges: []api.GroupMembership{},
|
||||
Secrets: []api.Secret{},
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
request.Name = currentName
|
||||
}
|
||||
|
||||
// Generate Group Membership changes based on current Group Memberships
|
||||
for _, operation := range operations {
|
||||
membership, err := getMembershipByUserID(currentMemberships, operation.UserID)
|
||||
if err != nil {
|
||||
// Membership does not Exist so we can only create a new one
|
||||
if operation.Delete {
|
||||
return fmt.Errorf("Cannot Delete User %v as it has no membership", operation.UserID)
|
||||
}
|
||||
request.GroupChanges = append(request.GroupChanges, api.GroupMembership{
|
||||
UserID: operation.UserID,
|
||||
IsAdmin: operation.IsGroupManager,
|
||||
})
|
||||
} else {
|
||||
// Membership Exists so we can modify or delete it
|
||||
if !operation.Delete && membership.IsAdmin == operation.IsGroupManager {
|
||||
return fmt.Errorf("Membership for User %v already Exists with Same Role", operation.UserID)
|
||||
}
|
||||
request.GroupChanges = append(request.GroupChanges, api.GroupMembership{
|
||||
ID: membership.ID,
|
||||
IsAdmin: operation.IsGroupManager,
|
||||
Delete: operation.Delete,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dryrun, err := c.UpdateGroupDryRun(ctx, groupID, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Update Group Dryrun: %w", err)
|
||||
}
|
||||
|
||||
var users []api.User
|
||||
// We can skip Getting users if we don't need to reencrypt any secrets
|
||||
if len(dryrun.DryRun.SecretsNeeded) != 0 {
|
||||
users, err = c.GetUsers(ctx, &api.GetUsersOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Getting Users: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The API gives it back nested so we just put it into a list here
|
||||
secrets := []api.Secret{}
|
||||
for _, container := range dryrun.DryRun.Secrets {
|
||||
secrets = append(secrets, container.Secret...)
|
||||
}
|
||||
|
||||
decryptedSecretCache := map[string]string{}
|
||||
for _, container := range dryrun.DryRun.SecretsNeeded {
|
||||
missingSecret := container.Secret
|
||||
// Deduplicate Secret Decrypting for when adding multiple users to a group
|
||||
if decryptedSecretCache[missingSecret.ResourceID] == "" {
|
||||
secret, err := getSecretByResourceID(secrets, missingSecret.ResourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Get Secret from Dryrun Response: %w", err)
|
||||
}
|
||||
|
||||
msg, err := c.DecryptMessage(secret.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Decrypting Secret: %w", err)
|
||||
}
|
||||
|
||||
decryptedSecretCache[missingSecret.ResourceID] = msg
|
||||
}
|
||||
|
||||
pubkey, err := getPublicKeyByUserID(missingSecret.UserID, users)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Get pubkey for User: %w", err)
|
||||
}
|
||||
|
||||
newSecretData, err := c.EncryptMessageWithPublicKey(pubkey, decryptedSecretCache[missingSecret.ResourceID])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Encrypting Secret: %w", err)
|
||||
}
|
||||
request.Secrets = append(request.Secrets, api.Secret{
|
||||
UserID: missingSecret.UserID,
|
||||
ResourceID: missingSecret.ResourceID,
|
||||
Data: newSecretData,
|
||||
})
|
||||
}
|
||||
|
||||
_, err = c.UpdateGroup(ctx, groupID, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Updating Group: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteGroup Deletes a Group
|
||||
func DeleteGroup(ctx context.Context, c *api.Client, groupID string) error {
|
||||
err := c.DeleteGroup(ctx, groupID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Deleting Group: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
54
helper/mfa.go
Normal file
54
helper/mfa.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
)
|
||||
|
||||
// AddMFACallbackTOTP adds a MFA callback to the client that generates OTP Codes on demand using a Token with configurable retries and delay
|
||||
func AddMFACallbackTOTP(c *api.Client, retrys uint, retryDelay, offset time.Duration, token string) {
|
||||
c.MFACallback = func(ctx context.Context, c *api.Client, res *api.APIResponse) (http.Cookie, error) {
|
||||
challenge := api.MFAChallenge{}
|
||||
err := json.Unmarshal(res.Body, &challenge)
|
||||
if err != nil {
|
||||
return http.Cookie{}, fmt.Errorf("Parsing MFA Challenge")
|
||||
}
|
||||
if challenge.Provider.TOTP == "" {
|
||||
return http.Cookie{}, fmt.Errorf("Server Provided no TOTP Provider")
|
||||
}
|
||||
for i := uint(0); i < retrys+1; i++ {
|
||||
var code string
|
||||
code, err = GenerateOTPCode(token, time.Now().Add(offset))
|
||||
if err != nil {
|
||||
return http.Cookie{}, fmt.Errorf("Error Generating MFA Code: %w", err)
|
||||
}
|
||||
req := api.MFAChallengeResponse{
|
||||
TOTP: code,
|
||||
}
|
||||
var raw *http.Response
|
||||
raw, _, err = c.DoCustomRequestAndReturnRawResponse(ctx, "POST", "mfa/verify/totp.json", "v2", req, nil)
|
||||
if err != nil {
|
||||
if errors.Unwrap(err) != api.ErrAPIResponseErrorStatusCode {
|
||||
return http.Cookie{}, fmt.Errorf("Doing MFA Challenge Response: %w", err)
|
||||
}
|
||||
// MFA failed, so lets wait just let the loop try again
|
||||
time.Sleep(retryDelay)
|
||||
} else {
|
||||
// MFA worked so lets find the cookie and return it
|
||||
for _, cookie := range raw.Cookies() {
|
||||
if cookie.Name == "passbolt_mfa" {
|
||||
return *cookie, nil
|
||||
}
|
||||
}
|
||||
return http.Cookie{}, fmt.Errorf("Unable to find Passbolt MFA Cookie")
|
||||
}
|
||||
}
|
||||
return http.Cookie{}, fmt.Errorf("Failed MFA Challenge 3 times: %w", err)
|
||||
}
|
||||
}
|
30
helper/resource_test.go
Normal file
30
helper/resource_test.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourceCreate(t *testing.T) {
|
||||
id, err := CreateResource(context.TODO(), client, "", "name", "username", "https://url.lan", "password123", "a password description")
|
||||
if err != nil {
|
||||
t.Fatalf("Creating Resource %v", err)
|
||||
}
|
||||
|
||||
_, name, username, uri, password, description, err := GetResource(context.TODO(), client, id)
|
||||
if err != nil {
|
||||
t.Fatalf("Getting Resource %v", err)
|
||||
}
|
||||
|
||||
equal(t, "Name", name, "name")
|
||||
equal(t, "Username", username, "username")
|
||||
equal(t, "URI", uri, "https://url.lan")
|
||||
equal(t, "Password", password, "password123")
|
||||
equal(t, "Description", description, "a password description")
|
||||
}
|
||||
|
||||
func equal(t *testing.T, name, a, b string) {
|
||||
if a != b {
|
||||
t.Fatalf("Value %v is %v instead of %v", name, a, b)
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/speatzle/go-passbolt/api"
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
)
|
||||
|
||||
// CreateResource Creates a Resource where the Password and Description are Encrypted and Returns the Resources ID
|
||||
|
@ -18,6 +18,7 @@ func CreateResource(ctx context.Context, c *api.Client, folderParentID, name, us
|
|||
for _, tmp := range types {
|
||||
if tmp.Slug == "password-and-description" {
|
||||
rType = &tmp
|
||||
break
|
||||
}
|
||||
}
|
||||
if rType == nil {
|
||||
|
@ -41,6 +42,11 @@ func CreateResource(ctx context.Context, c *api.Client, folderParentID, name, us
|
|||
return "", fmt.Errorf("Marshalling Secret Data: %w", err)
|
||||
}
|
||||
|
||||
err = validateSecretData(rType, string(secretData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Validating Secret Data: %w", err)
|
||||
}
|
||||
|
||||
encSecretData, err := c.EncryptMessage(string(secretData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Encrypting Secret Data for User me: %w", err)
|
||||
|
@ -94,8 +100,14 @@ func GetResource(ctx context.Context, c *api.Client, resourceID string) (folderP
|
|||
if err != nil {
|
||||
return "", "", "", "", "", "", fmt.Errorf("Getting Resource Secret: %w", err)
|
||||
}
|
||||
return GetResourceFromData(c, *resource, *secret, *rType)
|
||||
}
|
||||
|
||||
// GetResourceFromData Decrypts Resources using only local data, the Resource object must inlude the secret
|
||||
func GetResourceFromData(c *api.Client, resource api.Resource, secret api.Secret, rType api.ResourceType) (folderParentID, name, username, uri, password, description string, err error) {
|
||||
var pw string
|
||||
var desc string
|
||||
|
||||
switch rType.Slug {
|
||||
case "password-string":
|
||||
pw, err = c.DecryptMessage(secret.Data)
|
||||
|
@ -116,6 +128,21 @@ func GetResource(ctx context.Context, c *api.Client, resourceID string) (folderP
|
|||
}
|
||||
pw = secretData.Password
|
||||
desc = secretData.Description
|
||||
case "password-description-totp":
|
||||
rawSecretData, err := c.DecryptMessage(secret.Data)
|
||||
if err != nil {
|
||||
return "", "", "", "", "", "", fmt.Errorf("Decrypting Secret Data: %w", err)
|
||||
}
|
||||
|
||||
var secretData api.SecretDataTypePasswordDescriptionTOTP
|
||||
err = json.Unmarshal([]byte(rawSecretData), &secretData)
|
||||
if err != nil {
|
||||
return "", "", "", "", "", "", fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
|
||||
}
|
||||
pw = secretData.Password
|
||||
desc = secretData.Description
|
||||
case "totp":
|
||||
// nothing fits into the interface in this case
|
||||
default:
|
||||
return "", "", "", "", "", "", fmt.Errorf("Unknown ResourceType: %v", rType.Slug)
|
||||
}
|
||||
|
@ -136,7 +163,7 @@ func UpdateResource(ctx context.Context, c *api.Client, resourceID, name, userna
|
|||
}
|
||||
|
||||
opts := &api.GetUsersOptions{
|
||||
FilterHasAccess: resourceID,
|
||||
FilterHasAccess: []string{resourceID},
|
||||
}
|
||||
users, err := c.GetUsers(ctx, opts)
|
||||
if err != nil {
|
||||
|
@ -147,30 +174,127 @@ func UpdateResource(ctx context.Context, c *api.Client, resourceID, name, userna
|
|||
ID: resourceID,
|
||||
// This needs to be specified or it will revert to a legacy password
|
||||
ResourceTypeID: resource.ResourceTypeID,
|
||||
Name: name,
|
||||
Username: username,
|
||||
URI: uri,
|
||||
Name: resource.Name,
|
||||
Username: resource.Username,
|
||||
URI: resource.URI,
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
newResource.Name = name
|
||||
}
|
||||
if username != "" {
|
||||
newResource.Username = username
|
||||
}
|
||||
if uri != "" {
|
||||
newResource.URI = uri
|
||||
}
|
||||
|
||||
var secretData string
|
||||
switch rType.Slug {
|
||||
case "password-string":
|
||||
newResource.Description = description
|
||||
secretData = password
|
||||
newResource.Description = resource.Description
|
||||
if description != "" {
|
||||
newResource.Description = description
|
||||
}
|
||||
if password != "" {
|
||||
secretData = password
|
||||
} else {
|
||||
secret, err := c.GetSecret(ctx, resourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Getting Secret: %w", err)
|
||||
}
|
||||
secretData, err = c.DecryptMessage(secret.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Decrypting Secret: %w", err)
|
||||
}
|
||||
}
|
||||
case "password-and-description":
|
||||
tmp := api.SecretDataTypePasswordAndDescription{
|
||||
Password: password,
|
||||
Description: description,
|
||||
}
|
||||
if password != "" || description != "" {
|
||||
secret, err := c.GetSecret(ctx, resourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Getting Secret: %w", err)
|
||||
}
|
||||
oldSecretData, err := c.DecryptMessage(secret.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Decrypting Secret: %w", err)
|
||||
}
|
||||
var oldSecret api.SecretDataTypePasswordAndDescription
|
||||
err = json.Unmarshal([]byte(oldSecretData), &oldSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
|
||||
}
|
||||
if password == "" {
|
||||
tmp.Password = oldSecret.Password
|
||||
}
|
||||
if description == "" {
|
||||
tmp.Description = oldSecret.Description
|
||||
}
|
||||
}
|
||||
res, err := json.Marshal(&tmp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Marshalling Secret Data: %w", err)
|
||||
}
|
||||
secretData = string(res)
|
||||
case "password-description-totp":
|
||||
secret, err := c.GetSecret(ctx, resourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Getting Secret: %w", err)
|
||||
}
|
||||
oldSecretData, err := c.DecryptMessage(secret.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Decrypting Secret: %w", err)
|
||||
}
|
||||
var oldSecret api.SecretDataTypePasswordDescriptionTOTP
|
||||
err = json.Unmarshal([]byte(oldSecretData), &secretData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
|
||||
}
|
||||
if password != "" {
|
||||
oldSecret.Password = password
|
||||
}
|
||||
if description != "" {
|
||||
oldSecret.Description = description
|
||||
}
|
||||
|
||||
res, err := json.Marshal(&oldSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Marshalling Secret Data: %w", err)
|
||||
}
|
||||
secretData = string(res)
|
||||
case "totp":
|
||||
secret, err := c.GetSecret(ctx, resourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Getting Secret: %w", err)
|
||||
}
|
||||
oldSecretData, err := c.DecryptMessage(secret.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Decrypting Secret: %w", err)
|
||||
}
|
||||
var oldSecret api.SecretDataTypeTOTP
|
||||
err = json.Unmarshal([]byte(oldSecretData), &secretData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
|
||||
}
|
||||
// since we don't have totp parameters we don't do anything
|
||||
|
||||
res, err := json.Marshal(&oldSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Marshalling Secret Data: %w", err)
|
||||
}
|
||||
secretData = string(res)
|
||||
default:
|
||||
return fmt.Errorf("Unknown ResourceType: %v", rType.Slug)
|
||||
}
|
||||
|
||||
err = validateSecretData(rType, secretData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Validating Secret Data: %w", err)
|
||||
}
|
||||
|
||||
newResource.Secrets = []api.Secret{}
|
||||
for _, user := range users {
|
||||
var encSecretData string
|
||||
|
@ -201,10 +325,18 @@ func UpdateResource(ctx context.Context, c *api.Client, resourceID, name, userna
|
|||
|
||||
// DeleteResource Deletes a Resource
|
||||
func DeleteResource(ctx context.Context, c *api.Client, resourceID string) error {
|
||||
return c.DeleteResource(ctx, resourceID)
|
||||
err := c.DeleteResource(ctx, resourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Deleting Resource: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveResource Moves a Resource into a Folder
|
||||
func MoveResource(ctx context.Context, c *api.Client, resourceID, folderParentID string) error {
|
||||
return c.MoveResource(ctx, resourceID, folderParentID)
|
||||
err := c.MoveResource(ctx, resourceID, folderParentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Moveing Resource: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
66
helper/setup.go
Normal file
66
helper/setup.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/helper"
|
||||
)
|
||||
|
||||
// ParseInviteUrl Parses a Passbolt Invite URL into a user id and token
|
||||
func ParseInviteUrl(url string) (string, string, error) {
|
||||
split := strings.Split(url, "/")
|
||||
if len(split) < 4 {
|
||||
return "", "", fmt.Errorf("Invite URL does not have enough slashes")
|
||||
}
|
||||
return split[len(split)-2], strings.TrimSuffix(split[len(split)-1], ".json"), nil
|
||||
}
|
||||
|
||||
// SetupAccount Setup a Account for a Invited User.
|
||||
// (Use ParseInviteUrl to get the userid and token from a Invite URL)
|
||||
func SetupAccount(ctx context.Context, c *api.Client, userID, token, password string) (string, error) {
|
||||
|
||||
install, err := c.SetupInstall(ctx, userID, token)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Get Setup Install Data: %w", err)
|
||||
}
|
||||
|
||||
keyName := install.Profile.FirstName + " " + install.Profile.LastName + " " + install.Username
|
||||
|
||||
privateKey, err := helper.GenerateKey(keyName, install.Username, []byte(password), "rsa", 4096)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Generating Private Key: %w", err)
|
||||
}
|
||||
|
||||
key, err := crypto.NewKeyFromArmoredReader(strings.NewReader(privateKey))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Reading Private Key: %w", err)
|
||||
}
|
||||
|
||||
publicKey, err := key.GetArmoredPublicKey()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Get Public Key: %w", err)
|
||||
}
|
||||
|
||||
request := api.SetupCompleteRequest{
|
||||
AuthenticationToken: api.AuthenticationToken{
|
||||
Token: token,
|
||||
},
|
||||
User: api.User{
|
||||
Locale: api.UserLocaleENUK,
|
||||
},
|
||||
GPGKey: api.GPGKey{
|
||||
ArmoredKey: publicKey,
|
||||
},
|
||||
}
|
||||
|
||||
err = c.SetupComplete(ctx, userID, request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Setup Completion Failed: %w", err)
|
||||
}
|
||||
return privateKey, nil
|
||||
}
|
60
helper/setup_test.go
Normal file
60
helper/setup_test.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
)
|
||||
|
||||
var client *api.Client
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
url := os.Getenv("REG_URL")
|
||||
fmt.Printf("Registering with url: %v\n", url)
|
||||
userID, token, err := ParseInviteUrl(url)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Unable to Parse Invite URL: %w", err))
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
hc := &http.Client{Transport: tr}
|
||||
|
||||
rc, err := api.NewClient(hc, "", "https://localhost", "", "")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Creating Registration Client: %w", err))
|
||||
}
|
||||
|
||||
// Debug Output
|
||||
rc.Debug = true
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
privkey, err := SetupAccount(ctx, rc, userID, token, "password123")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Setup Account: %w", err))
|
||||
}
|
||||
|
||||
c, err := api.NewClient(hc, "", "https://localhost", privkey, "password123")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Setup Client: %w", err))
|
||||
}
|
||||
|
||||
// Debug Output
|
||||
c.Debug = true
|
||||
|
||||
c.Login(ctx)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Login Client: %w", err))
|
||||
}
|
||||
|
||||
client = c
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/speatzle/go-passbolt/api"
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
)
|
||||
|
||||
// ShareOperation defines how Resources are to be Shared With Users/Groups
|
||||
|
@ -63,14 +63,34 @@ func ShareResource(ctx context.Context, c *api.Client, resourceID string, change
|
|||
return fmt.Errorf("Decrypting Resource Secret: %w", err)
|
||||
}
|
||||
|
||||
// Secret Validation
|
||||
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)
|
||||
}
|
||||
|
||||
err = validateSecretData(rType, secretData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Validating Secret Data: %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)
|
||||
// if no users where added then we can skip this
|
||||
var users []api.User
|
||||
if len(simulationResult.Changes.Added) != 0 {
|
||||
users, err = c.GetUsers(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Get Users: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
shareRequest.Secrets = []api.Secret{}
|
||||
|
@ -122,12 +142,14 @@ func ShareFolderWithUsersAndGroups(ctx context.Context, c *api.Client, folderID
|
|||
// 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 *api.Client, folderID string, changes []ShareOperation) error {
|
||||
oldPermissions, err := c.GetFolderPermissions(ctx, folderID)
|
||||
oldFolder, err := c.GetFolder(ctx, folderID, &api.GetFolderOptions{
|
||||
ContainPermissions: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Getting Folder Permissions: %w", err)
|
||||
}
|
||||
|
||||
permissionChanges, err := GeneratePermissionChanges(oldPermissions, changes)
|
||||
permissionChanges, err := GeneratePermissionChanges(oldFolder.Permissions, changes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Generating Folder Permission Changes: %w", err)
|
||||
}
|
||||
|
@ -164,6 +186,7 @@ func GeneratePermissionChanges(oldPermissions []api.Permission, changes []ShareO
|
|||
for _, oldPerm := range oldPermissions {
|
||||
if oldPerm.ARO == change.ARO && oldPerm.AROForeignKey == change.AROID {
|
||||
oldPermission = &oldPerm
|
||||
break
|
||||
}
|
||||
}
|
||||
// Check Whether Matching Permission Already Exists and needs to be adjusted or is a new one can be created
|
||||
|
@ -207,12 +230,3 @@ func GeneratePermissionChanges(oldPermissions []api.Permission, changes []ShareO
|
|||
}
|
||||
return permissionChanges, nil
|
||||
}
|
||||
|
||||
func getPublicKeyByUserID(userID string, Users []api.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)
|
||||
}
|
||||
|
|
63
helper/totp.go
Normal file
63
helper/totp.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base32"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
mask1 = 0xf
|
||||
mask2 = 0x7f
|
||||
mask3 = 0xff
|
||||
timeSplitInSeconds = 30
|
||||
shift24 = 24
|
||||
shift16 = 16
|
||||
shift8 = 8
|
||||
codeLength = 6
|
||||
)
|
||||
|
||||
// GenerateOTPCode generates a 6 digit TOTP from the secret Token.
|
||||
func GenerateOTPCode(token string, when time.Time) (string, error) {
|
||||
timer := uint64(math.Floor(float64(when.Unix()) / float64(timeSplitInSeconds)))
|
||||
// Remove spaces, some providers are giving us in a readable format
|
||||
// so they add spaces in there. If it's not removed while pasting in,
|
||||
// remove it now.
|
||||
token = strings.Replace(token, " ", "", -1)
|
||||
|
||||
// It should be uppercase always
|
||||
token = strings.ToUpper(token)
|
||||
|
||||
// Remove all the extra "=" padding at the end
|
||||
token = strings.TrimRight(token, "=")
|
||||
|
||||
secretBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(token)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Decoding token string: %w", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 8)
|
||||
mac := hmac.New(sha1.New, secretBytes)
|
||||
|
||||
binary.BigEndian.PutUint64(buf, timer)
|
||||
_, _ = mac.Write(buf)
|
||||
sum := mac.Sum(nil)
|
||||
|
||||
// http://tools.ietf.org/html/rfc4226#section-5.4
|
||||
offset := sum[len(sum)-1] & mask1
|
||||
value := int64(((int(sum[offset]) & mask2) << shift24) |
|
||||
((int(sum[offset+1] & mask3)) << shift16) |
|
||||
((int(sum[offset+2] & mask3)) << shift8) |
|
||||
(int(sum[offset+3]) & mask3))
|
||||
|
||||
modulo := int32(value % int64(math.Pow10(codeLength)))
|
||||
|
||||
format := fmt.Sprintf("%%0%dd", codeLength)
|
||||
|
||||
return fmt.Sprintf(format, modulo), nil
|
||||
}
|
36
helper/totp_test.go
Normal file
36
helper/totp_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var testCases = []struct {
|
||||
description string
|
||||
token string
|
||||
expectErr bool
|
||||
}{
|
||||
{"generates otpcode from token with padding", "PGWXXL7B66MMSRBAWSKEKIYD3P675KRJ===", false},
|
||||
{"generates otpcode from token without padding", "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP", false},
|
||||
{"invalid token format", "INVALIDTOKEN123", true},
|
||||
}
|
||||
|
||||
func TestGenerateOTPCode(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
code, err := GenerateOTPCode(tc.token, time.Now())
|
||||
|
||||
if tc.expectErr {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for input '%s', but got none", tc.token)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("GenerateOTPCode returned an error: %s", err.Error())
|
||||
} else if len(code) != 6 {
|
||||
t.Errorf("Expected 6-digit OTP, got: %s", code)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
109
helper/user.go
Normal file
109
helper/user.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
)
|
||||
|
||||
// CreateUser Creates a new User
|
||||
func CreateUser(ctx context.Context, c *api.Client, role, username, firstname, lastname string) (string, error) {
|
||||
roles, err := c.GetRoles(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Get Role: %w", err)
|
||||
}
|
||||
|
||||
roleID := ""
|
||||
|
||||
for _, r := range roles {
|
||||
if r.Name == role {
|
||||
roleID = r.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if roleID == "" {
|
||||
return "", fmt.Errorf("Cannot Find Role: %v", role)
|
||||
}
|
||||
|
||||
u, err := c.CreateUser(ctx, api.User{
|
||||
Username: username,
|
||||
Profile: &api.Profile{
|
||||
FirstName: firstname,
|
||||
LastName: lastname,
|
||||
},
|
||||
RoleID: roleID,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Creating User: %w", err)
|
||||
}
|
||||
return u.ID, nil
|
||||
}
|
||||
|
||||
// GetUser Gets a User
|
||||
func GetUser(ctx context.Context, c *api.Client, userID string) (string, string, string, string, error) {
|
||||
u, err := c.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return "", "", "", "", fmt.Errorf("Getting User: %w", err)
|
||||
}
|
||||
return u.Role.Name, u.Username, u.Profile.FirstName, u.Profile.LastName, nil
|
||||
}
|
||||
|
||||
// UpdateUser Updates a User
|
||||
func UpdateUser(ctx context.Context, c *api.Client, userID, role, firstname, lastname string) error {
|
||||
user, err := c.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Getting User: %w", err)
|
||||
}
|
||||
|
||||
new := api.User{
|
||||
Profile: &api.Profile{
|
||||
FirstName: user.Profile.FirstName,
|
||||
LastName: user.Profile.LastName,
|
||||
},
|
||||
}
|
||||
|
||||
if role != "" {
|
||||
roles, err := c.GetRoles(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Get Role: %w", err)
|
||||
}
|
||||
|
||||
roleID := ""
|
||||
|
||||
for _, r := range roles {
|
||||
if r.Name == role {
|
||||
roleID = r.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if roleID == "" {
|
||||
return fmt.Errorf("Cannot Find Role %v", role)
|
||||
}
|
||||
new.RoleID = roleID
|
||||
}
|
||||
|
||||
if firstname != "" {
|
||||
new.Profile.FirstName = firstname
|
||||
}
|
||||
if lastname != "" {
|
||||
new.Profile.LastName = lastname
|
||||
}
|
||||
|
||||
_, err = c.UpdateUser(ctx, userID, new)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Updating User: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUser Deletes a User
|
||||
func DeleteUser(ctx context.Context, c *api.Client, userID string) error {
|
||||
err := c.DeleteUser(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Deleting User: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
79
helper/util.go
Normal file
79
helper/util.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/passbolt/go-passbolt/api"
|
||||
"github.com/santhosh-tekuri/jsonschema"
|
||||
)
|
||||
|
||||
func getPublicKeyByUserID(userID string, Users []api.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)
|
||||
}
|
||||
|
||||
func getMembershipByUserID(memberships []api.GroupMembership, userID string) (*api.GroupMembership, error) {
|
||||
for _, membership := range memberships {
|
||||
if membership.UserID == userID {
|
||||
return &membership, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Cannot Find Membership for user id %v", userID)
|
||||
}
|
||||
|
||||
func getSecretByResourceID(secrets []api.Secret, resourceID string) (*api.Secret, error) {
|
||||
for _, secret := range secrets {
|
||||
if secret.ResourceID == resourceID {
|
||||
return &secret, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Cannot Find Secret for id %v", resourceID)
|
||||
}
|
||||
|
||||
func validateSecretData(rType *api.ResourceType, secretData string) error {
|
||||
var schemaDefinition api.ResourceTypeSchema
|
||||
err := json.Unmarshal([]byte(rType.Definition), &schemaDefinition)
|
||||
if err != nil {
|
||||
// Workaround for inconsistant API Responses where sometime the Schema is embedded directly and sometimes it's escaped as a string
|
||||
if err.Error() == "json: cannot unmarshal string into Go value of type api.ResourceTypeSchema" {
|
||||
var tmp string
|
||||
err = json.Unmarshal([]byte(rType.Definition), &tmp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Workaround Unmarshal Json Schema String: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(tmp), &schemaDefinition)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Workaround Unmarshal Json Schema: %w", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("Unmarshal Json Schema: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
comp := jsonschema.NewCompiler()
|
||||
|
||||
err = comp.AddResource("secret.json", bytes.NewReader(schemaDefinition.Secret))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Adding Json Schema: %w", err)
|
||||
}
|
||||
|
||||
schema, err := comp.Compile("secret.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Compiling Json Schema: %w", err)
|
||||
}
|
||||
|
||||
err = schema.Validate(strings.NewReader(secretData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Validating Secret Data: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Add table
Reference in a new issue