actions.go 6.44 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 10

	"git.autistici.org/ai3/go-common/pwhash"
11
	"git.autistici.org/id/go-sso"
ale's avatar
ale committed
12
	"github.com/pquerna/otp/totp"
13
	"github.com/sethvargo/go-password/password"
ale's avatar
ale committed
14 15
)

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
// 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)
}

// 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.
	SSO      *sso.Ticket
	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.
52
type RequestBase struct {
53
	SSO string `json:"sso"`
54 55 56 57 58

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

59 60 61 62
// Sanitize the request.
func (r *RequestBase) Sanitize() {
	if r.SSO != "" {
		r.SSO = sanitizedValue
63
	}
64 65
}

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

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

83 84 85 86
// 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
87

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

93 94 95 96
// Authorize the request.
func (r *AdminRequestBase) Authorize(rctx *RequestContext) error {
	if !rctx.isAdmin(rctx.SSO) {
		return fmt.Errorf("user %s is not an admin", rctx.SSO.User)
97 98 99 100
	}
	return nil
}

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

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

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

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

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

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

144 145 146 147 148
// PrivilegedRequestBase extends RequestBase with the user password,
// for privileged endpoints.
type PrivilegedRequestBase struct {
	UserRequestBase
	CurPassword string `json:"cur_password"`
149 150
}

151 152 153 154 155
// Sanitize the request.
func (r *PrivilegedRequestBase) Sanitize() {
	r.UserRequestBase.Sanitize()
	if r.CurPassword != "" {
		r.CurPassword = sanitizedValue
ale's avatar
ale committed
156
	}
157 158
}

159 160 161 162
// 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
163
	}
164 165
	// TODO: call out to the auth-server
	if !pwhash.ComparePassword(rctx.User.Password, r.CurPassword) {
166
		return errors.New("bad password")
ale's avatar
ale committed
167
	}
168
	return nil
ale's avatar
ale committed
169 170
}

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

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

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

195
	return r.RequestBase.PopulateContext(rctx)
ale's avatar
ale committed
196 197
}

198 199
// Authorize the request.
func (r *ResourceRequestBase) Authorize(rctx *RequestContext) error {
ale's avatar
ale committed
200
	if !rctx.isAdmin(rctx.SSO) && !rctx.TX.CanAccessResource(rctx.Context, rctx.SSO.User, rctx.Resource) {
201 202 203
		return fmt.Errorf("user %s can't access resource %s", rctx.SSO.User, rctx.Resource.ID)
	}
	return nil
ale's avatar
ale committed
204 205
}

206 207 208
// AdminResourceRequestBase is an admin-only version of ResourceRequestBase.
type AdminResourceRequestBase struct {
	ResourceRequestBase
209 210
}

211 212 213 214
// Authorize the request.
func (r *AdminResourceRequestBase) Authorize(rctx *RequestContext) error {
	if !rctx.isAdmin(rctx.SSO) {
		return fmt.Errorf("user %s is not an admin", rctx.SSO.User)
215 216 217 218
	}
	return nil
}

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

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

238 239
const appSpecificPasswordIDLen = 4

ale's avatar
ale committed
240
func randomAppSpecificPasswordID() string {
241
	return randomBase64(appSpecificPasswordIDLen)
ale's avatar
ale committed
242
}
ale's avatar
ale committed
243 244 245 246 247 248 249 250

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