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

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

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

13 14 15 16 17 18 19 20 21 22 23
// 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)
}

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

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
// 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.
45
	Auth     Auth
46 47 48 49 50 51 52 53 54
	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.
55
type RequestBase struct {
56
	SSO string `json:"sso"`
57 58 59 60 61

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

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

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

74 75
// PopulateContext extracts information from the request and stores it
// into the RequestContext.
76 77 78
func (r *RequestBase) PopulateContext(rctx *RequestContext) (err error) {
	rctx.Comment = r.Comment
	rctx.Auth, err = rctx.authFromSSO(r.SSO)
ale's avatar
ale committed
79
	if err != nil {
80
		return
ale's avatar
ale committed
81
	}
82
	return
ale's avatar
ale committed
83
}
ale's avatar
ale committed
84

85 86 87 88
// 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
89

90 91
// AdminRequestBase is a generic admin request.
type AdminRequestBase struct {
ale's avatar
ale committed
92 93 94
	RequestBase
}

95 96
// Authorize the request.
func (r *AdminRequestBase) Authorize(rctx *RequestContext) error {
97 98
	if !rctx.Auth.IsAdmin {
		return fmt.Errorf("user %s is not an admin", rctx.Auth.Username)
99 100 101 102
	}
	return nil
}

103 104
// UserRequestBase is a generic request about a specific user.
type UserRequestBase struct {
ale's avatar
ale committed
105
	RequestBase
106
	Username string `json:"username"`
ale's avatar
ale committed
107 108
}

ale's avatar
ale committed
109
// Validate the request.
110 111
func (r *UserRequestBase) Validate(rctx *RequestContext) error {
	return r.RequestBase.Validate(rctx)
ale's avatar
ale committed
112 113
}

114 115 116 117 118 119
// 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
120
	}
121 122
	rctx.User = user
	return r.RequestBase.PopulateContext(rctx)
ale's avatar
ale committed
123 124
}

125 126
// Authorize the request.
func (r *UserRequestBase) Authorize(rctx *RequestContext) error {
127 128
	if !rctx.Auth.IsAdmin && rctx.Auth.Username != r.Username {
		return fmt.Errorf("user %s can't access resource %s", rctx.Auth.Username, r.Username)
129 130 131 132
	}
	return nil
}

133 134 135
// AdminUserRequestBase is an admin-only version of UserRequestBase.
type AdminUserRequestBase struct {
	UserRequestBase
ale's avatar
ale committed
136 137
}

138 139
// Authorize the request.
func (r *AdminUserRequestBase) Authorize(rctx *RequestContext) error {
140 141
	if !rctx.Auth.IsAdmin {
		return fmt.Errorf("user %s is not an admin", rctx.Auth.Username)
ale's avatar
ale committed
142
	}
ale's avatar
ale committed
143
	return nil
ale's avatar
ale committed
144 145
}

146 147 148 149 150
// PrivilegedRequestBase extends RequestBase with the user password,
// for privileged endpoints.
type PrivilegedRequestBase struct {
	UserRequestBase
	CurPassword string `json:"cur_password"`
151 152
	RemoteAddr  string `json:"remote_addr"`
	// TODO: Add full DeviceInfo?
153 154
}

155 156 157 158 159
// Sanitize the request.
func (r *PrivilegedRequestBase) Sanitize() {
	r.UserRequestBase.Sanitize()
	if r.CurPassword != "" {
		r.CurPassword = sanitizedValue
ale's avatar
ale committed
160
	}
161 162
}

163 164 165 166
// 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
167
	}
168 169
	if err := rctx.authorizeUser(rctx.Context, rctx.User.Name, r.CurPassword, r.RemoteAddr); err != nil {
		return err
ale's avatar
ale committed
170
	}
171
	return nil
ale's avatar
ale committed
172 173
}

174 175 176 177
// ResourceRequestBase is the base type for resource-level requests.
type ResourceRequestBase struct {
	RequestBase
	ResourceID ResourceID `json:"resource_id"`
178 179
}

180 181 182 183
// 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
184 185 186
	if err != nil {
		return err
	}
ale's avatar
ale committed
187
	rctx.Resource = &rsrc.Resource
ale's avatar
ale committed
188

189
	// If the resource has an owner, populate the User context field.
ale's avatar
ale committed
190 191
	if rsrc.Owner != "" {
		user, err := getUserOrDie(rctx.Context, rctx.TX, rsrc.Owner)
192
		if err != nil {
ale's avatar
ale committed
193 194
			return err
		}
195
		rctx.User = user
ale's avatar
ale committed
196 197
	}

198
	return r.RequestBase.PopulateContext(rctx)
ale's avatar
ale committed
199 200
}

201 202
// Authorize the request.
func (r *ResourceRequestBase) Authorize(rctx *RequestContext) error {
203 204
	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)
205 206
	}
	return nil
ale's avatar
ale committed
207 208
}

209 210 211
// AdminResourceRequestBase is an admin-only version of ResourceRequestBase.
type AdminResourceRequestBase struct {
	ResourceRequestBase
212 213
}

214 215
// Authorize the request.
func (r *AdminResourceRequestBase) Authorize(rctx *RequestContext) error {
216 217
	if !rctx.Auth.IsAdmin {
		return fmt.Errorf("user %s is not an admin", rctx.Auth.Username)
218 219 220 221
	}
	return nil
}

222
func randomBase64(n int) string {
223
	b := make([]byte, n*3/4)
ale's avatar
ale committed
224
	_, err := rand.Read(b[:]) // #nosec
225 226 227
	if err != nil {
		panic(err)
	}
228 229 230 231 232 233
	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)
234 235
}

ale's avatar
ale committed
236
func randomAppSpecificPassword() string {
ale's avatar
ale committed
237 238
	// Create a 32-character password with 5 digits and 5 symbols.
	return password.MustGenerate(32, 5, 5, false, false)
ale's avatar
ale committed
239 240
}

241 242
const appSpecificPasswordIDLen = 4

ale's avatar
ale committed
243
func randomAppSpecificPasswordID() string {
244
	return randomBase64(appSpecificPasswordIDLen)
ale's avatar
ale committed
245
}
ale's avatar
ale committed
246 247 248 249 250 251 252 253

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