aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2021-11-21 20:54:22 -0800
committerMike Crute <mike@crute.us>2021-11-21 20:54:22 -0800
commit3a5a7e108d9b20f7ef6a7e4bb8439f6e2ba65fa5 (patch)
treef0a0b65473ada73988c8e4aa73261486ac96a158
parent0049bdd2ab6b6b743e9a0cf89f6cbabc8b08e2d4 (diff)
downloadcloud-identity-broker-3a5a7e108d9b20f7ef6a7e4bb8439f6e2ba65fa5.tar.bz2
cloud-identity-broker-3a5a7e108d9b20f7ef6a7e4bb8439f6e2ba65fa5.tar.xz
cloud-identity-broker-3a5a7e108d9b20f7ef6a7e4bb8439f6e2ba65fa5.zip
Add cloud account CRUD endpoints
-rw-r--r--app/controllers/api_account.go207
-rw-r--r--app/controllers/api_account_list.go2
-rw-r--r--app/models/account.go95
-rw-r--r--cloud/aws/aws.go35
-rw-r--r--cmd/web/server.go8
-rw-r--r--go.mod4
-rw-r--r--go.sum8
7 files changed, 322 insertions, 37 deletions
diff --git a/app/controllers/api_account.go b/app/controllers/api_account.go
new file mode 100644
index 0000000..259a7d4
--- /dev/null
+++ b/app/controllers/api_account.go
@@ -0,0 +1,207 @@
1package controllers
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "time"
8
9 "code.crute.us/mcrute/cloud-identity-broker/app/middleware"
10 "code.crute.us/mcrute/cloud-identity-broker/app/models"
11 "code.crute.us/mcrute/cloud-identity-broker/cloud/aws"
12
13 glecho "code.crute.us/mcrute/golib/echo"
14 "code.crute.us/mcrute/golib/echo/controller"
15 "github.com/labstack/echo/v4"
16)
17
18type APIAccountHandler struct {
19 Store models.AccountStore
20 AdminStore models.AccountStore
21}
22
23func (h *APIAccountHandler) Register(prefix string, r glecho.URLRouter, mw ...echo.MiddlewareFunc) {
24 // This resource did not exist in the V1 API and thus has no V1
25 // representation. We use the default handlers for V1 because otherwise
26 // requests with V1 Accept headers would result in 406 Unacceptable errors.
27 gh := &controller.ContentTypeNegotiatingHandler{
28 DefaultHandler: h.HandleGet,
29 Handlers: map[string]echo.HandlerFunc{
30 contentTypeV1: h.HandleGet,
31 contentTypeV2: h.HandleGet,
32 },
33 }
34 r.GET(prefix, gh.Handle, mw...)
35
36 ph := &controller.ContentTypeNegotiatingHandler{
37 DefaultHandler: h.HandlePut,
38 Handlers: map[string]echo.HandlerFunc{
39 contentTypeV1: h.HandlePut,
40 contentTypeV2: h.HandlePut,
41 },
42 }
43 r.PUT(prefix, ph.Handle, mw...)
44
45 poh := &controller.ContentTypeNegotiatingHandler{
46 DefaultHandler: h.HandlePost,
47 Handlers: map[string]echo.HandlerFunc{
48 contentTypeV1: h.HandlePost,
49 contentTypeV2: h.HandlePost,
50 },
51 }
52 r.POST(prefix, poh.Handle, mw...)
53
54 r.DELETE(prefix, h.HandleDelete, mw...)
55}
56
57func (h *APIAccountHandler) getPrincipalAndAccount(c echo.Context) (*models.User, *models.Account, error) {
58 var err error
59 ctx := context.Background()
60
61 p, err := middleware.GetAuthorizedPrincipal(c)
62 if err != nil {
63 return nil, nil, echo.ErrUnauthorized
64 }
65
66 var a *models.Account
67 if p.IsAdmin {
68 a, err = h.AdminStore.GetForUser(ctx, c.Param("account"), p)
69 if err != nil {
70 return nil, nil, echo.NotFoundHandler(c)
71 }
72 } else {
73 a, err = h.Store.GetForUser(ctx, c.Param("account"), p)
74 if err != nil {
75 return nil, nil, echo.NotFoundHandler(c)
76 }
77 }
78
79 return p, a, nil
80}
81
82func (h *APIAccountHandler) HandleGet(c echo.Context) error {
83 p, a, err := h.getPrincipalAndAccount(c)
84 if err != nil {
85 return err
86 }
87
88 // These fields are slightly sensitive and give away too many security
89 // details about the account so they should only be visible to users who
90 // can administer the account.
91 if !a.CanBeModifiedBy(p) {
92 a.VaultMaterial = ""
93 a.Users = nil
94 }
95
96 return c.JSON(http.StatusOK, a)
97}
98
99func (h *APIAccountHandler) HandlePut(c echo.Context) error {
100 var in models.Account
101 if err := c.Echo().JSONSerializer.Deserialize(c, &in); err != nil {
102 return echo.ErrBadRequest
103 }
104
105 p, a, err := h.getPrincipalAndAccount(c)
106 if err != nil {
107 return err
108 }
109
110 if !a.CanBeModifiedBy(p) {
111 return echo.ErrForbidden
112 }
113
114 if in.ShortName != a.ShortName {
115 return &echo.HTTPError{
116 Code: http.StatusBadRequest,
117 Message: "Account short_name can not be changed. Create a new account.",
118 }
119 }
120
121 if in.AccountType != a.AccountType {
122 return &echo.HTTPError{
123 Code: http.StatusBadRequest,
124 Message: "Account type can not be changed. Create a new account.",
125 }
126 }
127
128 a.AccountNumber = in.AccountNumber
129 a.Name = in.Name
130 a.ConsoleSessionDuration = in.ConsoleSessionDuration
131 a.VaultMaterial = in.VaultMaterial
132 a.DefaultRegion = in.DefaultRegion
133 a.Users = in.Users
134
135 // PUT-ing Deleted equal to null effectively un-deletes the record
136 a.Deleted = in.Deleted
137
138 if err := h.Store.Put(context.Background(), a); err != nil {
139 return echo.ErrInternalServerError
140 }
141
142 return c.String(http.StatusNoContent, "")
143}
144
145func (h *APIAccountHandler) HandlePost(c echo.Context) error {
146 var in models.Account
147 if err := c.Echo().JSONSerializer.Deserialize(c, &in); err != nil {
148 return echo.ErrBadRequest
149 }
150
151 if _, err := h.AdminStore.Get(context.Background(), in.ShortName); err == nil {
152 return &echo.HTTPError{
153 Code: http.StatusConflict,
154 Message: "Account with short name already exists. Choose another short name.",
155 }
156 }
157
158 if in.ConsoleSessionDuration < time.Hour {
159 in.ConsoleSessionDuration = time.Hour
160 }
161
162 if in.ConsoleSessionDuration > 12*time.Hour {
163 return &echo.HTTPError{
164 Code: http.StatusBadRequest,
165 Message: "Console duration is greater than the AWS maximum of 12 hours.",
166 }
167 }
168
169 if in.Deleted != nil {
170 return &echo.HTTPError{
171 Code: http.StatusBadRequest,
172 Message: "Can not create deleted account, set Deleted to null",
173 }
174 }
175
176 if err := aws.ValidateVaultMaterial(in.VaultMaterial); err != nil {
177 return &echo.HTTPError{
178 Code: http.StatusBadRequest,
179 Message: fmt.Sprintf("Unable to access Vault material: %s", err),
180 }
181 }
182
183 if err := h.Store.Put(context.Background(), &in); err != nil {
184 return echo.ErrInternalServerError
185 }
186
187 c.Response().Header().Add("Location", glecho.URLFor(c, "/api/account", in.ShortName).String())
188
189 return c.String(http.StatusCreated, "")
190}
191
192func (h *APIAccountHandler) HandleDelete(c echo.Context) error {
193 p, a, err := h.getPrincipalAndAccount(c)
194 if err != nil {
195 return err
196 }
197
198 if a.CanBeModifiedBy(p) {
199 if err := h.Store.Delete(context.Background(), a); err != nil {
200 return echo.ErrInternalServerError
201 }
202 } else {
203 return echo.ErrForbidden
204 }
205
206 return c.String(http.StatusNoContent, "")
207}
diff --git a/app/controllers/api_account_list.go b/app/controllers/api_account_list.go
index f69db6a..28b64c1 100644
--- a/app/controllers/api_account_list.go
+++ b/app/controllers/api_account_list.go
@@ -17,6 +17,7 @@ type jsonAccount struct {
17 AccountNumber int `json:"account_number"` 17 AccountNumber int `json:"account_number"`
18 ShortName string `json:"short_name"` 18 ShortName string `json:"short_name"`
19 Name string `json:"name"` 19 Name string `json:"name"`
20 SelfUrl string `json:"url"`
20 ConsoleUrl string `json:"get_console_url,omitempty"` 21 ConsoleUrl string `json:"get_console_url,omitempty"`
21 ConsoleRedirectUrl string `json:"console_redirect_url,omitempty"` 22 ConsoleRedirectUrl string `json:"console_redirect_url,omitempty"`
22 CredentialsUrl string `json:"credentials_url"` 23 CredentialsUrl string `json:"credentials_url"`
@@ -28,6 +29,7 @@ func jsonAccountFromAccount(c echo.Context, a *models.Account) *jsonAccount {
28 AccountNumber: a.AccountNumber, 29 AccountNumber: a.AccountNumber,
29 ShortName: a.ShortName, 30 ShortName: a.ShortName,
30 Name: a.Name, 31 Name: a.Name,
32 SelfUrl: glecho.URLFor(c, "/api/account", a.ShortName).String(),
31 ConsoleUrl: glecho.URLFor(c, "/api/account", a.ShortName, "console").String(), 33 ConsoleUrl: glecho.URLFor(c, "/api/account", a.ShortName, "console").String(),
32 ConsoleRedirectUrl: glecho.URLFor(c, "/api/account", a.ShortName, "console").Query("redirect", "1").String(), 34 ConsoleRedirectUrl: glecho.URLFor(c, "/api/account", a.ShortName, "console").Query("redirect", "1").String(),
33 CredentialsUrl: glecho.URLFor(c, "/api/account", a.ShortName, "credentials").String(), 35 CredentialsUrl: glecho.URLFor(c, "/api/account", a.ShortName, "credentials").String(),
diff --git a/app/models/account.go b/app/models/account.go
index 0ae1821..61b144d 100644
--- a/app/models/account.go
+++ b/app/models/account.go
@@ -2,10 +2,11 @@ package models
2 2
3import ( 3import (
4 "context" 4 "context"
5 "fmt"
6 "time" 5 "time"
7 6
8 "code.crute.us/mcrute/golib/db/mongodb" 7 "code.crute.us/mcrute/golib/db/mongodb"
8 "go.mongodb.org/mongo-driver/bson"
9 "go.mongodb.org/mongo-driver/bson/primitive"
9) 10)
10 11
11const accountCol = "accounts" 12const accountCol = "accounts"
@@ -20,43 +21,48 @@ type AccountStore interface {
20} 21}
21 22
22type Account struct { 23type Account struct {
23 ShortName string `bson:"_id"` 24 ShortName string `bson:"_id" json:"short_name"`
24 AccountType string 25 AccountType string `json:"account_type"`
25 AccountNumber int 26 AccountNumber int `json:"account_number"`
26 Name string 27 Name string `json:"name"`
27 ConsoleSessionDuration time.Duration 28 ConsoleSessionDuration time.Duration `json:"console_session_duration, omitempty"`
28 VaultMaterial string 29 VaultMaterial string `json:"vault_material,omitempty"`
29 DefaultRegion string 30 DefaultRegion string `json:"default_region"`
30 Users []string 31 Users []string `json:"users,omitempty"`
32 Deleted *time.Time `json:"deleted,omitempty" bson:"deleted,omitempty"`
31} 33}
32 34
33func (a *Account) ConsoleSessionDurationSecs() int64 { 35func (a *Account) ConsoleSessionDurationSecs() int64 {
34 return int64(a.ConsoleSessionDuration.Seconds()) 36 return int64(a.ConsoleSessionDuration.Seconds())
35} 37}
36 38
37func (a *Account) CanAccess(u *User) bool { 39func (a *Account) CanBeModifiedBy(u *User) bool {
38 if u.IsAdmin { 40 return u.IsAdmin
39 return true
40 }
41 // Linear search should be fine for now, these lists are pretty small
42 for _, n := range a.Users {
43 if n == u.Username {
44 return true
45 }
46 }
47 return false
48} 41}
49 42
50type MongoDbAccountStore struct { 43type MongoDbAccountStore struct {
51 Db *mongodb.Mongo 44 Db *mongodb.Mongo
45
46 // ReturnDeleted will allow all methods to return deleted items. By default
47 // items where the Deleted field is set will not be returned. This should
48 // be the common cast for most code using this store but in some Admin
49 // use-cases it would be useful to show deleted accounts.
50 ReturnDeleted bool
52} 51}
53 52
54// List returns all accounts in the system. 53// List returns all accounts in the system.
55func (s *MongoDbAccountStore) List(ctx context.Context) ([]*Account, error) { 54func (s *MongoDbAccountStore) List(ctx context.Context) ([]*Account, error) {
56 var out []*Account 55 var out []*Account
57 if err := s.Db.FindAll(ctx, accountCol, &out); err != nil { 56
57 filter := bson.M{}
58 if !s.ReturnDeleted {
59 filter["deleted"] = primitive.Null{}
60 }
61
62 if err := s.Db.FindAllByFilter(ctx, accountCol, filter, &out); err != nil {
58 return nil, err 63 return nil, err
59 } 64 }
65
60 return out, nil 66 return out, nil
61} 67}
62 68
@@ -68,34 +74,56 @@ func (s *MongoDbAccountStore) List(ctx context.Context) ([]*Account, error) {
68// just use List directly. 74// just use List directly.
69func (s *MongoDbAccountStore) ListForUser(ctx context.Context, u *User) ([]*Account, error) { 75func (s *MongoDbAccountStore) ListForUser(ctx context.Context, u *User) ([]*Account, error) {
70 var out []*Account 76 var out []*Account
71 filter := mongodb.AnyInTopLevelArray("Users", u.Username) 77
78 filter := mongodb.AnyInTopLevelArray("users", u.Username)
79 if !s.ReturnDeleted {
80 filter["deleted"] = primitive.Null{}
81 }
82
72 if err := s.Db.FindAllByFilter(ctx, accountCol, filter, &out); err != nil { 83 if err := s.Db.FindAllByFilter(ctx, accountCol, filter, &out); err != nil {
73 return nil, err 84 return nil, err
74 } 85 }
86
75 return out, nil 87 return out, nil
76} 88}
77 89
78func (s *MongoDbAccountStore) Get(ctx context.Context, id string) (*Account, error) { 90func (s *MongoDbAccountStore) Get(ctx context.Context, id string) (*Account, error) {
79 var a Account 91 var a Account
80 if err := s.Db.FindOneById(ctx, accountCol, id, &a); err != nil { 92
93 filter := bson.M{"_id": id}
94 if !s.ReturnDeleted {
95 filter["deleted"] = primitive.Null{}
96 }
97
98 if err := s.Db.FindOneByFilter(ctx, accountCol, filter, &a); err != nil {
81 return nil, err 99 return nil, err
82 } 100 }
101
83 return &a, nil 102 return &a, nil
84} 103}
85 104
86// GetForUser returns an account if the user has access to this account, 105// GetForUser returns an account if the user has access to this account,
87// otherwise it returns an error. This is the authorized version of Get. 106// otherwise it returns an error. This is the authorized version of Get.
88func (s *MongoDbAccountStore) GetForUser(ctx context.Context, id string, u *User) (*Account, error) { 107func (s *MongoDbAccountStore) GetForUser(ctx context.Context, id string, u *User) (*Account, error) {
89 a, err := s.Get(ctx, id) 108 var a Account
90 if err != nil { 109 var filter bson.M
91 return nil, err 110
111 if u.IsAdmin {
112 filter = bson.M{"_id": id}
113 } else {
114 filter = mongodb.AnyInTopLevelArray("users", u.Username)
115 filter["_id"] = id
92 } 116 }
93 117
94 if !a.CanAccess(u) { 118 if !s.ReturnDeleted {
95 return nil, fmt.Errorf("User does not have access to account") 119 filter["deleted"] = primitive.Null{}
96 } 120 }
97 121
98 return a, nil 122 if err := s.Db.FindOneByFilter(ctx, accountCol, filter, &a); err != nil {
123 return nil, err
124 }
125
126 return &a, nil
99} 127}
100 128
101func (s *MongoDbAccountStore) Put(ctx context.Context, a *Account) error { 129func (s *MongoDbAccountStore) Put(ctx context.Context, a *Account) error {
@@ -106,10 +134,15 @@ func (s *MongoDbAccountStore) Put(ctx context.Context, a *Account) error {
106} 134}
107 135
108func (s *MongoDbAccountStore) Delete(ctx context.Context, a *Account) error { 136func (s *MongoDbAccountStore) Delete(ctx context.Context, a *Account) error {
109 if err := s.Db.DeleteOneById(ctx, accountCol, a.ShortName); err != nil { 137 a, err := s.Get(ctx, a.ShortName)
138 if err != nil {
110 return err 139 return err
111 } 140 }
112 return nil 141
142 now := time.Now()
143 a.Deleted = &now
144
145 return s.Put(ctx, a)
113} 146}
114 147
115var _ AccountStore = (*MongoDbAccountStore)(nil) 148var _ AccountStore = (*MongoDbAccountStore)(nil)
diff --git a/cloud/aws/aws.go b/cloud/aws/aws.go
index 180b2c4..36ac338 100644
--- a/cloud/aws/aws.go
+++ b/cloud/aws/aws.go
@@ -82,6 +82,41 @@ func NewAWSClientFromAccount(a *models.Account) (AWSClient, error) {
82 }, nil 82 }, nil
83} 83}
84 84
85// ValidateVaultMaterial is used to check that a Vault material can be accessed
86// and that the shape of that material is correct for an AWS access key and
87// role list.
88//
89// This should be used for admission control for the creation of new accounts.
90func ValidateVaultMaterial(m string) error {
91 var ac account
92 if err := vault.GetVaultKeyStruct(m, &ac); err != nil {
93 return fmt.Errorf("Unable to access vault material: %w", err)
94 }
95
96 if ac.AccessKeyId == "" {
97 return fmt.Errorf("AccessKeyId is empty")
98 }
99
100 if ac.SecretAccessKey == "" {
101 return fmt.Errorf("SecretAccessKey is empty")
102 }
103
104 if len(ac.Roles) == 0 {
105 return fmt.Errorf("No roles specified")
106 }
107
108 for k, r := range ac.Roles {
109 if r.ARN == "" {
110 return fmt.Errorf("ARN for role %s is empty", k)
111 }
112 if r.ExternalId == "" {
113 return fmt.Errorf("ExternalId for role %s is empty", k)
114 }
115 }
116
117 return nil
118}
119
85// AssumeRole uses an IAM user credential with higher privilege to assume a 120// AssumeRole uses an IAM user credential with higher privilege to assume a
86// role in an AWS account and region. It returns the STS credentials. 121// role in an AWS account and region. It returns the STS credentials.
87// 122//
diff --git a/cmd/web/server.go b/cmd/web/server.go
index d13cd58..d2ea861 100644
--- a/cmd/web/server.go
+++ b/cmd/web/server.go
@@ -103,6 +103,10 @@ func webMain(cfg app.Config, embeddedTemplates fs.FS, version string) {
103 ), 103 ),
104 ) 104 )
105 105
106 adminAccountStore := &models.MongoDbAccountStore{
107 Db: mongo,
108 ReturnDeleted: true,
109 }
106 as := &models.MongoDbAccountStore{Db: mongo} 110 as := &models.MongoDbAccountStore{Db: mongo}
107 us := &models.MongoDbUserStore{Db: mongo} 111 us := &models.MongoDbUserStore{Db: mongo}
108 112
@@ -143,6 +147,10 @@ func webMain(cfg app.Config, embeddedTemplates fs.FS, version string) {
143 controllers.NewAPICredentialsHandler(aws), 147 controllers.NewAPICredentialsHandler(aws),
144 rateLimit, 148 rateLimit,
145 ) 149 )
150 (&controllers.APIAccountHandler{
151 Store: as,
152 AdminStore: adminAccountStore,
153 }).Register("/:account", api)
146 } 154 }
147 s.GET("/favicon.ico", echo.NotFoundHandler) 155 s.GET("/favicon.ico", echo.NotFoundHandler)
148 s.GET("/logout", controllers.LogoutHandler) 156 s.GET("/logout", controllers.LogoutHandler)
diff --git a/go.mod b/go.mod
index c819322..df20f18 100644
--- a/go.mod
+++ b/go.mod
@@ -5,8 +5,8 @@ go 1.17
5require ( 5require (
6 code.crute.us/mcrute/golib v0.3.0 6 code.crute.us/mcrute/golib v0.3.0
7 code.crute.us/mcrute/golib/cli v0.1.2 7 code.crute.us/mcrute/golib/cli v0.1.2
8 code.crute.us/mcrute/golib/db/mongodb v0.2.0 8 code.crute.us/mcrute/golib/db/mongodb v0.3.0
9 code.crute.us/mcrute/golib/echo v0.5.0 9 code.crute.us/mcrute/golib/echo v0.5.1
10 code.crute.us/mcrute/golib/vault v0.1.2 10 code.crute.us/mcrute/golib/vault v0.1.2
11 github.com/aws/aws-sdk-go v1.42.4 11 github.com/aws/aws-sdk-go v1.42.4
12 github.com/labstack/echo/v4 v4.6.1 12 github.com/labstack/echo/v4 v4.6.1
diff --git a/go.sum b/go.sum
index ffbddbe..3f63d02 100644
--- a/go.sum
+++ b/go.sum
@@ -41,10 +41,10 @@ code.crute.us/mcrute/golib v0.3.0 h1:7g45xUf/din4VZkKAIK+bPCfXVTlwnZYS4Jv+/lUQ74
41code.crute.us/mcrute/golib v0.3.0/go.mod h1:VOnYQYqBYC3NUYPKwbzYSHW/BUBBU5RX7Z+A9nlJZUc= 41code.crute.us/mcrute/golib v0.3.0/go.mod h1:VOnYQYqBYC3NUYPKwbzYSHW/BUBBU5RX7Z+A9nlJZUc=
42code.crute.us/mcrute/golib/cli v0.1.2 h1:Yeg+8Jcm5FSYxFvebIGGmDJqmaDCgxl6QPb0GTTMXi0= 42code.crute.us/mcrute/golib/cli v0.1.2 h1:Yeg+8Jcm5FSYxFvebIGGmDJqmaDCgxl6QPb0GTTMXi0=
43code.crute.us/mcrute/golib/cli v0.1.2/go.mod h1:qhim2CV3zsMflpCbTMJs7dKnzzVIdBkSvm4jHDyXgik= 43code.crute.us/mcrute/golib/cli v0.1.2/go.mod h1:qhim2CV3zsMflpCbTMJs7dKnzzVIdBkSvm4jHDyXgik=
44code.crute.us/mcrute/golib/db/mongodb v0.2.0 h1:tumWZET3BgkutMWkeLLhIYy0c7dgEHa6mABF6oE1Y1o= 44code.crute.us/mcrute/golib/db/mongodb v0.3.0 h1:YBvVoFDqO1nqZFeYa1GwiCgkK8+LoQd03n61VV2AYWg=
45code.crute.us/mcrute/golib/db/mongodb v0.2.0/go.mod h1:JUX7PU8mUu68Y4sOERbZKON+x5A7cIxgxifxpXw//Bs= 45code.crute.us/mcrute/golib/db/mongodb v0.3.0/go.mod h1:JUX7PU8mUu68Y4sOERbZKON+x5A7cIxgxifxpXw//Bs=
46code.crute.us/mcrute/golib/echo v0.5.0 h1:M8D69fCopxLee4rTYmPswNe2OVI7x7OmQOsr5P9nyUU= 46code.crute.us/mcrute/golib/echo v0.5.1 h1:YMQroWqNXlLzo6Zpz967F3rEyaO+5AFaiJ+PKLalFoA=
47code.crute.us/mcrute/golib/echo v0.5.0/go.mod h1:rNrjiYlJDwkabv0alUpSIsS/tR6HeBoP90UHaPUiL+Q= 47code.crute.us/mcrute/golib/echo v0.5.1/go.mod h1:rNrjiYlJDwkabv0alUpSIsS/tR6HeBoP90UHaPUiL+Q=
48code.crute.us/mcrute/golib/vault v0.1.1/go.mod h1:kr+P3q7WJ/+dKieQJ3ZMccWeWV0M3KyHu6Ofghn0H7U= 48code.crute.us/mcrute/golib/vault v0.1.1/go.mod h1:kr+P3q7WJ/+dKieQJ3ZMccWeWV0M3KyHu6Ofghn0H7U=
49code.crute.us/mcrute/golib/vault v0.1.2 h1:L80WffgReTtL8FUV83GRAYjPzhAQo4G+h1y1n3CkZEU= 49code.crute.us/mcrute/golib/vault v0.1.2 h1:L80WffgReTtL8FUV83GRAYjPzhAQo4G+h1y1n3CkZEU=
50code.crute.us/mcrute/golib/vault v0.1.2/go.mod h1:kr+P3q7WJ/+dKieQJ3ZMccWeWV0M3KyHu6Ofghn0H7U= 50code.crute.us/mcrute/golib/vault v0.1.2/go.mod h1:kr+P3q7WJ/+dKieQJ3ZMccWeWV0M3KyHu6Ofghn0H7U=