diff options
author | Mike Crute <mike@crute.us> | 2021-11-21 20:54:22 -0800 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2021-11-21 20:54:22 -0800 |
commit | 3a5a7e108d9b20f7ef6a7e4bb8439f6e2ba65fa5 (patch) | |
tree | f0a0b65473ada73988c8e4aa73261486ac96a158 | |
parent | 0049bdd2ab6b6b743e9a0cf89f6cbabc8b08e2d4 (diff) | |
download | cloud-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.go | 207 | ||||
-rw-r--r-- | app/controllers/api_account_list.go | 2 | ||||
-rw-r--r-- | app/models/account.go | 95 | ||||
-rw-r--r-- | cloud/aws/aws.go | 35 | ||||
-rw-r--r-- | cmd/web/server.go | 8 | ||||
-rw-r--r-- | go.mod | 4 | ||||
-rw-r--r-- | go.sum | 8 |
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 @@ | |||
1 | package controllers | ||
2 | |||
3 | import ( | ||
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 | |||
18 | type APIAccountHandler struct { | ||
19 | Store models.AccountStore | ||
20 | AdminStore models.AccountStore | ||
21 | } | ||
22 | |||
23 | func (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 | |||
57 | func (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 | |||
82 | func (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 | |||
99 | func (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 | |||
145 | func (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 | |||
192 | func (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 | ||
3 | import ( | 3 | import ( |
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 | ||
11 | const accountCol = "accounts" | 12 | const accountCol = "accounts" |
@@ -20,43 +21,48 @@ type AccountStore interface { | |||
20 | } | 21 | } |
21 | 22 | ||
22 | type Account struct { | 23 | type 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 | ||
33 | func (a *Account) ConsoleSessionDurationSecs() int64 { | 35 | func (a *Account) ConsoleSessionDurationSecs() int64 { |
34 | return int64(a.ConsoleSessionDuration.Seconds()) | 36 | return int64(a.ConsoleSessionDuration.Seconds()) |
35 | } | 37 | } |
36 | 38 | ||
37 | func (a *Account) CanAccess(u *User) bool { | 39 | func (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 | ||
50 | type MongoDbAccountStore struct { | 43 | type 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. |
55 | func (s *MongoDbAccountStore) List(ctx context.Context) ([]*Account, error) { | 54 | func (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. |
69 | func (s *MongoDbAccountStore) ListForUser(ctx context.Context, u *User) ([]*Account, error) { | 75 | func (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 | ||
78 | func (s *MongoDbAccountStore) Get(ctx context.Context, id string) (*Account, error) { | 90 | func (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. |
88 | func (s *MongoDbAccountStore) GetForUser(ctx context.Context, id string, u *User) (*Account, error) { | 107 | func (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 | ||
101 | func (s *MongoDbAccountStore) Put(ctx context.Context, a *Account) error { | 129 | func (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 | ||
108 | func (s *MongoDbAccountStore) Delete(ctx context.Context, a *Account) error { | 136 | func (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 | ||
115 | var _ AccountStore = (*MongoDbAccountStore)(nil) | 148 | var _ 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. | ||
90 | func 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) |
@@ -5,8 +5,8 @@ go 1.17 | |||
5 | require ( | 5 | require ( |
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 |
@@ -41,10 +41,10 @@ code.crute.us/mcrute/golib v0.3.0 h1:7g45xUf/din4VZkKAIK+bPCfXVTlwnZYS4Jv+/lUQ74 | |||
41 | code.crute.us/mcrute/golib v0.3.0/go.mod h1:VOnYQYqBYC3NUYPKwbzYSHW/BUBBU5RX7Z+A9nlJZUc= | 41 | code.crute.us/mcrute/golib v0.3.0/go.mod h1:VOnYQYqBYC3NUYPKwbzYSHW/BUBBU5RX7Z+A9nlJZUc= |
42 | code.crute.us/mcrute/golib/cli v0.1.2 h1:Yeg+8Jcm5FSYxFvebIGGmDJqmaDCgxl6QPb0GTTMXi0= | 42 | code.crute.us/mcrute/golib/cli v0.1.2 h1:Yeg+8Jcm5FSYxFvebIGGmDJqmaDCgxl6QPb0GTTMXi0= |
43 | code.crute.us/mcrute/golib/cli v0.1.2/go.mod h1:qhim2CV3zsMflpCbTMJs7dKnzzVIdBkSvm4jHDyXgik= | 43 | code.crute.us/mcrute/golib/cli v0.1.2/go.mod h1:qhim2CV3zsMflpCbTMJs7dKnzzVIdBkSvm4jHDyXgik= |
44 | code.crute.us/mcrute/golib/db/mongodb v0.2.0 h1:tumWZET3BgkutMWkeLLhIYy0c7dgEHa6mABF6oE1Y1o= | 44 | code.crute.us/mcrute/golib/db/mongodb v0.3.0 h1:YBvVoFDqO1nqZFeYa1GwiCgkK8+LoQd03n61VV2AYWg= |
45 | code.crute.us/mcrute/golib/db/mongodb v0.2.0/go.mod h1:JUX7PU8mUu68Y4sOERbZKON+x5A7cIxgxifxpXw//Bs= | 45 | code.crute.us/mcrute/golib/db/mongodb v0.3.0/go.mod h1:JUX7PU8mUu68Y4sOERbZKON+x5A7cIxgxifxpXw//Bs= |
46 | code.crute.us/mcrute/golib/echo v0.5.0 h1:M8D69fCopxLee4rTYmPswNe2OVI7x7OmQOsr5P9nyUU= | 46 | code.crute.us/mcrute/golib/echo v0.5.1 h1:YMQroWqNXlLzo6Zpz967F3rEyaO+5AFaiJ+PKLalFoA= |
47 | code.crute.us/mcrute/golib/echo v0.5.0/go.mod h1:rNrjiYlJDwkabv0alUpSIsS/tR6HeBoP90UHaPUiL+Q= | 47 | code.crute.us/mcrute/golib/echo v0.5.1/go.mod h1:rNrjiYlJDwkabv0alUpSIsS/tR6HeBoP90UHaPUiL+Q= |
48 | code.crute.us/mcrute/golib/vault v0.1.1/go.mod h1:kr+P3q7WJ/+dKieQJ3ZMccWeWV0M3KyHu6Ofghn0H7U= | 48 | code.crute.us/mcrute/golib/vault v0.1.1/go.mod h1:kr+P3q7WJ/+dKieQJ3ZMccWeWV0M3KyHu6Ofghn0H7U= |
49 | code.crute.us/mcrute/golib/vault v0.1.2 h1:L80WffgReTtL8FUV83GRAYjPzhAQo4G+h1y1n3CkZEU= | 49 | code.crute.us/mcrute/golib/vault v0.1.2 h1:L80WffgReTtL8FUV83GRAYjPzhAQo4G+h1y1n3CkZEU= |
50 | code.crute.us/mcrute/golib/vault v0.1.2/go.mod h1:kr+P3q7WJ/+dKieQJ3ZMccWeWV0M3KyHu6Ofghn0H7U= | 50 | code.crute.us/mcrute/golib/vault v0.1.2/go.mod h1:kr+P3q7WJ/+dKieQJ3ZMccWeWV0M3KyHu6Ofghn0H7U= |