actions.go 6.63 KB
Newer Older
ale's avatar
ale committed
1
2
3
4
package accountserver

import (
	"context"
5
6
	"crypto/rand"
	"encoding/base64"
7
	"errors"
8
	"fmt"
ale's avatar
ale committed
9

ale's avatar
ale committed
10
	"github.com/pquerna/otp/totp"
11
	"github.com/sethvargo/go-password/password"
ale's avatar
ale committed
12
13
)

14
15
16
17
18
19
20
21
22
23
24
// Request is the generic interface for request types. Each request
// type defines its own handler, built of composable objects that
// define its behavior with respect to validation, authentication and
// execution.
type Request interface {
	PopulateContext(*RequestContext) error
	Validate(*RequestContext) error
	Authorize(*RequestContext) error
	Serve(*RequestContext) (interface{}, error)
}

25
26
27
28
29
30
// Auth parameters of an incoming request (validated).
type Auth struct {
	Username string
	IsAdmin  bool
}

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// The RequestContext holds a large number of request-scoped variables
// populated at different stages of the action workflow. This is
// simpler than managing lots of custom Context vars and the
// associated boilerplate, but it's still a bit of an antipattern due
// to the loss of generality.
type RequestContext struct {
	// Link to the infra services.
	*AccountService

	// Request-scoped read-only values.
	Context context.Context
	//HTTPRequest *http.Request
	TX TX

	// Request-scoped read-write parameters.
46
	Auth     Auth
47
48
49
50
51
52
53
54
55
	User     *RawUser
	Resource *Resource
	Comment  string
}

// Value that we put in place of private fields when sanitizing.
const sanitizedValue = "XXXXXX"

// RequestBase contains parameters shared by all authenticated request types.
56
type RequestBase struct {
57
	SSO string `json:"sso"`
58
59
60
61
62

	// Optional comment, will end up in audit logs.
	Comment string `json:"comment,omitempty"`
}

63
64
65
66
// Sanitize the request.
func (r *RequestBase) Sanitize() {
	if r.SSO != "" {
		r.SSO = sanitizedValue
67
	}
68
69
}

ale's avatar
ale committed
70
// Validate the request.
71
func (r *RequestBase) Validate(rctx *RequestContext) error {
72
73
74
	return nil
}

75
76
// PopulateContext extracts information from the request and stores it
// into the RequestContext.
77
func (r *RequestBase) PopulateContext(rctx *RequestContext) error {
78
	rctx.Comment = r.Comment
79
80

	if r.SSO == "" {
81
		return newAuthError(errors.New("no SSO credentials provided"))
82
	}
83
	var err error
84
	rctx.Auth, err = rctx.authFromSSO(r.SSO)
ale's avatar
ale committed
85
	if err != nil {
86
		return newAuthError(err)
ale's avatar
ale committed
87
	}
88
	return nil
ale's avatar
ale committed
89
}
ale's avatar
ale committed
90

91
92
93
94
// commented out so subtypes *must* implement authorization, checked at compile time.
//func (r *RequestBase) Authorize(rctx *RequestContext) error {
//	return nil
//}
ale's avatar
ale committed
95

96
97
// AdminRequestBase is a generic admin request.
type AdminRequestBase struct {
ale's avatar
ale committed
98
99
100
	RequestBase
}

101
102
// Authorize the request.
func (r *AdminRequestBase) Authorize(rctx *RequestContext) error {
103
104
	if !rctx.Auth.IsAdmin {
		return fmt.Errorf("user %s is not an admin", rctx.Auth.Username)
105
106
107
108
	}
	return nil
}

109
110
// UserRequestBase is a generic request about a specific user.
type UserRequestBase struct {
ale's avatar
ale committed
111
	RequestBase
112
	Username string `json:"username"`
ale's avatar
ale committed
113
114
}

ale's avatar
ale committed
115
// Validate the request.
116
117
func (r *UserRequestBase) Validate(rctx *RequestContext) error {
	return r.RequestBase.Validate(rctx)
ale's avatar
ale committed
118
119
}

120
121
122
123
124
125
// PopulateContext extracts information from the request and stores it
// into the RequestContext.
func (r *UserRequestBase) PopulateContext(rctx *RequestContext) error {
	user, err := getUserOrDie(rctx.Context, rctx.TX, r.Username)
	if err != nil {
		return err
126
	}
127
128
	rctx.User = user
	return r.RequestBase.PopulateContext(rctx)
ale's avatar
ale committed
129
130
}

131
132
// Authorize the request.
func (r *UserRequestBase) Authorize(rctx *RequestContext) error {
133
134
	if !rctx.Auth.IsAdmin && rctx.Auth.Username != r.Username {
		return fmt.Errorf("user %s can't access resource %s", rctx.Auth.Username, r.Username)
135
136
137
138
	}
	return nil
}

139
140
141
// AdminUserRequestBase is an admin-only version of UserRequestBase.
type AdminUserRequestBase struct {
	UserRequestBase
ale's avatar
ale committed
142
143
}

144
145
// Authorize the request.
func (r *AdminUserRequestBase) Authorize(rctx *RequestContext) error {
146
147
	if !rctx.Auth.IsAdmin {
		return fmt.Errorf("user %s is not an admin", rctx.Auth.Username)
ale's avatar
ale committed
148
	}
ale's avatar
ale committed
149
	return nil
ale's avatar
ale committed
150
151
}

152
153
154
155
156
// PrivilegedRequestBase extends RequestBase with the user password,
// for privileged endpoints.
type PrivilegedRequestBase struct {
	UserRequestBase
	CurPassword string `json:"cur_password"`
157
158
	RemoteAddr  string `json:"remote_addr"`
	// TODO: Add full DeviceInfo?
159
160
}

161
162
163
164
165
// Sanitize the request.
func (r *PrivilegedRequestBase) Sanitize() {
	r.UserRequestBase.Sanitize()
	if r.CurPassword != "" {
		r.CurPassword = sanitizedValue
ale's avatar
ale committed
166
	}
167
168
}

169
170
171
172
// Authorize the request.
func (r *PrivilegedRequestBase) Authorize(rctx *RequestContext) error {
	if err := r.UserRequestBase.Authorize(rctx); err != nil {
		return err
ale's avatar
ale committed
173
	}
174
175
	if err := rctx.authorizeUser(rctx.Context, rctx.User.Name, r.CurPassword, r.RemoteAddr); err != nil {
		return err
ale's avatar
ale committed
176
	}
177
	return nil
ale's avatar
ale committed
178
179
}

180
181
182
183
// ResourceRequestBase is the base type for resource-level requests.
type ResourceRequestBase struct {
	RequestBase
	ResourceID ResourceID `json:"resource_id"`
184
185
}

186
187
188
189
// PopulateContext extracts information from the request and stores it
// into the RequestContext.
func (r *ResourceRequestBase) PopulateContext(rctx *RequestContext) error {
	rsrc, err := getResourceOrDie(rctx.Context, rctx.TX, r.ResourceID)
ale's avatar
ale committed
190
191
192
	if err != nil {
		return err
	}
ale's avatar
ale committed
193
	rctx.Resource = &rsrc.Resource
ale's avatar
ale committed
194

195
	// If the resource has an owner, populate the User context field.
ale's avatar
ale committed
196
197
	if rsrc.Owner != "" {
		user, err := getUserOrDie(rctx.Context, rctx.TX, rsrc.Owner)
198
		if err != nil {
ale's avatar
ale committed
199
200
			return err
		}
201
		rctx.User = user
ale's avatar
ale committed
202
203
	}

204
	return r.RequestBase.PopulateContext(rctx)
ale's avatar
ale committed
205
206
}

207
208
// Authorize the request.
func (r *ResourceRequestBase) Authorize(rctx *RequestContext) error {
209
210
	if !rctx.Auth.IsAdmin && !rctx.TX.CanAccessResource(rctx.Context, rctx.Auth.Username, rctx.Resource) {
		return fmt.Errorf("user %s can't access resource %s", rctx.Auth.Username, rctx.Resource.ID)
211
212
	}
	return nil
ale's avatar
ale committed
213
214
}

215
216
217
// AdminResourceRequestBase is an admin-only version of ResourceRequestBase.
type AdminResourceRequestBase struct {
	ResourceRequestBase
218
219
}

220
221
// Authorize the request.
func (r *AdminResourceRequestBase) Authorize(rctx *RequestContext) error {
222
223
	if !rctx.Auth.IsAdmin {
		return fmt.Errorf("user %s is not an admin", rctx.Auth.Username)
224
225
226
227
	}
	return nil
}

228
func randomBase64(n int) string {
229
	b := make([]byte, n*3/4)
ale's avatar
ale committed
230
	_, err := rand.Read(b[:]) // #nosec
231
232
233
	if err != nil {
		panic(err)
	}
234
235
236
237
238
239
	return base64.StdEncoding.EncodeToString(b[:])[:n]
}

func randomPassword() string {
	// Create a 16-character password with 4 digits and 2 symbols.
	return password.MustGenerate(16, 4, 2, false, false)
240
241
}

ale's avatar
ale committed
242
func randomAppSpecificPassword() string {
ale's avatar
ale committed
243
244
	// Create a 32-character password with 5 digits and 5 symbols.
	return password.MustGenerate(32, 5, 5, false, false)
ale's avatar
ale committed
245
246
}

247
248
const appSpecificPasswordIDLen = 4

ale's avatar
ale committed
249
func randomAppSpecificPasswordID() string {
250
	return randomBase64(appSpecificPasswordIDLen)
ale's avatar
ale committed
251
}
ale's avatar
ale committed
252
253
254
255
256
257
258
259

func generateTOTPSecret() (string, error) {
	key, err := totp.Generate(totp.GenerateOpts{})
	if err != nil {
		return "", err
	}
	return key.Secret(), nil
}