1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-08 12:31:58 +00:00

New JWT features and changes (examples updated). Improvements on the Context User and Private Error features

TODO: Write the new e-book JWT section and the HISTORY entry of the chnages and  add a simple example on site docs
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-10-17 06:40:17 +03:00
parent b816156e77
commit 1864f99145
19 changed files with 1749 additions and 493 deletions

View File

@@ -714,9 +714,10 @@ func (ctx *Context) StopWithError(statusCode int, err error) {
}
ctx.SetErr(err)
if IsErrPrivate(err) {
// error is private, we can't render it, instead .
// let the error handler render the code text.
if _, ok := err.(ErrPrivate); ok {
// error is private, we SHOULD not render it,
// leave the error handler alone to
// render the code's text instead.
ctx.StopWithStatus(statusCode)
return
}
@@ -5065,8 +5066,6 @@ func (ctx *Context) IsDebug() bool {
return ctx.app.IsDebug()
}
const errorContextKey = "iris.context.error"
// SetErr is just a helper that sets an error value
// as a context value, it does nothing more.
// Also, by-default this error's value is written to the client
@@ -5088,14 +5087,71 @@ func (ctx *Context) SetErr(err error) {
// GetErr is a helper which retrieves
// the error value stored by `SetErr`.
//
// Note that, if an error was stored by `SetErrPrivate`
// then it returns the underline/original error instead
// of the internal error wrapper.
func (ctx *Context) GetErr() error {
_, err := ctx.GetErrPublic()
return err
}
// ErrPrivate if provided then the error saved in context
// should NOT be visible to the client no matter what.
type ErrPrivate interface {
error
IrisPrivateError()
}
// An internal wrapper for the `SetErrPrivate` method.
type privateError struct{ error }
func (e privateError) IrisPrivateError() {}
// PrivateError accepts an error and returns a wrapped private one.
func PrivateError(err error) ErrPrivate {
if err == nil {
return nil
}
errPrivate, ok := err.(ErrPrivate)
if !ok {
errPrivate = privateError{err}
}
return errPrivate
}
const errorContextKey = "iris.context.error"
// SetErrPrivate sets an error that it's only accessible through `GetErr`
// and it should never be sent to the client.
//
// Same as ctx.SetErr with an error that completes the `ErrPrivate` interface.
// See `GetErrPublic` too.
func (ctx *Context) SetErrPrivate(err error) {
ctx.SetErr(PrivateError(err))
}
// GetErrPublic reports whether the stored error
// can be displayed to the client without risking
// to expose security server implementation to the client.
//
// If the error is not nil, it is always the original one.
func (ctx *Context) GetErrPublic() (bool, error) {
if v := ctx.values.Get(errorContextKey); v != nil {
if err, ok := v.(error); ok {
return err
switch err := v.(type) {
case privateError:
// If it's an error set by SetErrPrivate then unwrap it.
return false, err.error
case ErrPrivate:
return false, err
case error:
return true, err
}
}
return nil
return false, nil
}
// ErrPanicRecovery may be returned from `Context` actions of a `Handler`
@@ -5135,22 +5191,6 @@ func IsErrPanicRecovery(err error) (*ErrPanicRecovery, bool) {
return v, ok
}
// ErrPrivate if provided then the error saved in context
// should NOT be visible to the client no matter what.
type ErrPrivate interface {
IrisPrivateError()
}
// IsErrPrivate reports whether the given "err" is a private one.
func IsErrPrivate(err error) bool {
if err == nil {
return false
}
_, ok := err.(ErrPrivate)
return ok
}
// IsRecovered reports whether this handler has been recovered
// by the Iris recover middleware.
func (ctx *Context) IsRecovered() (*ErrPanicRecovery, bool) {

View File

@@ -2,7 +2,9 @@ package context
import (
"errors"
"strings"
"time"
"unicode"
)
// ErrNotSupported is fired when a specific method is not implemented
@@ -21,6 +23,13 @@ var ErrNotSupported = errors.New("not supported")
//
// The caller is free to cast this with the implementation directly
// when special features are offered by the authorization system.
//
// To make optional some of the fields you can just embed the User interface
// and implement whatever methods you want to support.
//
// There are two builtin implementations of the User interface:
// - SimpleUser (type-safe)
// - UserMap (wraps a map[string]interface{})
type User interface {
// GetAuthorization should return the authorization method,
// e.g. Basic Authentication.
@@ -35,7 +44,33 @@ type User interface {
GetPassword() string
// GetEmail should return the e-mail of the User.
GetEmail() string
}
// GetRoles should optionally return the specific user's roles.
// Returns `ErrNotSupported` if this method is not
// implemented by the User implementation.
GetRoles() ([]string, error)
// GetToken should optionally return a token used
// to authorize this User.
GetToken() (string, error)
// GetField should optionally return a dynamic field
// based on its key. Useful for custom user fields.
// Keep in mind that these fields are encoded as a separate JSON key.
GetField(key string) (interface{}, error)
} /* Notes:
We could use a structure of User wrapper and separate interfaces for each of the methods
so they return ErrNotSupported if the implementation is missing it, so the `Features`
field and HasUserFeature can be omitted and
add a Raw() interface{} to return the underline User implementation too.
The advandages of the above idea is that we don't have to add new methods
for each of the builtin features and we can keep the (assumed) struct small.
But we dont as it has many disadvantages, unless is requested.
The disadvantage of the current implementation is that the developer MUST
complete the whole interface in order to be a valid User and if we add
new methods in the future their implementation will break
(unless they have a static interface implementation check as we have on SimpleUser).
We kind of by-pass this disadvantage by providing a SimpleUser which can be embedded (as pointer)
to the end-developer's custom implementations.
*/
// FeaturedUser optional interface that a User can implement.
type FeaturedUser interface {
@@ -55,6 +90,9 @@ const (
UsernameFeature
PasswordFeature
EmailFeature
RolesFeature
TokenFeature
FieldsFeature
)
// HasUserFeature reports whether the "u" User
@@ -80,13 +118,16 @@ func HasUserFeature(user User, feature UserFeature) (bool, error) {
type SimpleUser struct {
Authorization string `json:"authorization"`
AuthorizedAt time.Time `json:"authorized_at"`
Username string `json:"username"`
Username string `json:"username,omitempty"`
Password string `json:"-"`
Email string `json:"email,omitempty"`
Features []UserFeature `json:"-"`
Roles []string `json:"roles,omitempty"`
Features []UserFeature `json:"features,omitempty"`
Token string `json:"token,omitempty"`
Fields Map `json:"fields,omitempty"`
}
var _ User = (*SimpleUser)(nil)
var _ FeaturedUser = (*SimpleUser)(nil)
// GetAuthorization returns the authorization method,
// e.g. Basic Authentication.
@@ -115,6 +156,39 @@ func (u *SimpleUser) GetEmail() string {
return u.Email
}
// GetRoles returns the specific user's roles.
// Returns with `ErrNotSupported` if the Roles field is not initialized.
func (u *SimpleUser) GetRoles() ([]string, error) {
if u.Roles == nil {
return nil, ErrNotSupported
}
return u.Roles, nil
}
// GetToken returns the token associated with this User.
// It may return empty if the User is not featured with a Token.
//
// The implementation can change that behavior.
// Returns with `ErrNotSupported` if the Token field is empty.
func (u *SimpleUser) GetToken() (string, error) {
if u.Token == "" {
return "", ErrNotSupported
}
return u.Token, nil
}
// GetField optionally returns a dynamic field from the `Fields` field
// based on its key.
func (u *SimpleUser) GetField(key string) (interface{}, error) {
if u.Fields == nil {
return nil, ErrNotSupported
}
return u.Fields[key], nil
}
// GetFeatures returns a list of features
// this User implementation offers.
func (u *SimpleUser) GetFeatures() []UserFeature {
@@ -140,5 +214,159 @@ func (u *SimpleUser) GetFeatures() []UserFeature {
features = append(features, EmailFeature)
}
if u.Roles != nil {
features = append(features, RolesFeature)
}
if u.Fields != nil {
features = append(features, FieldsFeature)
}
return features
}
// UserMap can be used to convert a common map[string]interface{} to a User.
// Usage:
// user := map[string]interface{}{
// "username": "kataras",
// "age" : 27,
// }
// ctx.SetUser(UserMap(user))
// OR
// user := UserMap{"key": "value",...}
// ctx.SetUser(user)
// [...]
// username := ctx.User().GetUsername()
// age := ctx.User().GetField("age").(int)
// OR cast it:
// user := ctx.User().(UserMap)
// username := user["username"].(string)
// age := user["age"].(int)
type UserMap Map
var _ FeaturedUser = UserMap{}
// GetAuthorization returns the authorization or Authorization value of the map.
func (u UserMap) GetAuthorization() string {
return u.str("authorization")
}
// GetAuthorizedAt returns the authorized_at or Authorized_At value of the map.
func (u UserMap) GetAuthorizedAt() time.Time {
return u.time("authorized_at")
}
// GetUsername returns the username or Username value of the map.
func (u UserMap) GetUsername() string {
return u.str("username")
}
// GetPassword returns the password or Password value of the map.
func (u UserMap) GetPassword() string {
return u.str("password")
}
// GetEmail returns the email or Email value of the map.
func (u UserMap) GetEmail() string {
return u.str("email")
}
// GetRoles returns the roles or Roles value of the map.
func (u UserMap) GetRoles() ([]string, error) {
if s := u.strSlice("roles"); s != nil {
return s, nil
}
return nil, ErrNotSupported
}
// GetToken returns the roles or Roles value of the map.
func (u UserMap) GetToken() (string, error) {
if s := u.str("token"); s != "" {
return s, nil
}
return "", ErrNotSupported
}
// GetField returns the raw map's value based on its "key".
// It's not kind of useful here as you can just use the map.
func (u UserMap) GetField(key string) (interface{}, error) {
return u[key], nil
}
// GetFeatures returns a list of features
// this map offers.
func (u UserMap) GetFeatures() []UserFeature {
if v := u.val("features"); v != nil { // if already contain features.
if features, ok := v.([]UserFeature); ok {
return features
}
}
// else try to resolve from map values.
features := []UserFeature{FieldsFeature}
if !u.GetAuthorizedAt().IsZero() {
features = append(features, AuthorizedAtFeature)
}
if u.GetUsername() != "" {
features = append(features, UsernameFeature)
}
if u.GetPassword() != "" {
features = append(features, PasswordFeature)
}
if u.GetEmail() != "" {
features = append(features, EmailFeature)
}
if roles, err := u.GetRoles(); err == nil && roles != nil {
features = append(features, RolesFeature)
}
return features
}
func (u UserMap) val(key string) interface{} {
isTitle := unicode.IsTitle(rune(key[0])) // if starts with uppercase.
if isTitle {
key = strings.ToLower(key)
}
return u[key]
}
func (u UserMap) str(key string) string {
if v := u.val(key); v != nil {
if s, ok := v.(string); ok {
return s
}
// exists or not we don't care, if it's invalid type we don't fill it.
}
return ""
}
func (u UserMap) strSlice(key string) []string {
if v := u.val(key); v != nil {
if s, ok := v.([]string); ok {
return s
}
}
return nil
}
func (u UserMap) time(key string) time.Time {
if v := u.val(key); v != nil {
if t, ok := v.(time.Time); ok {
return t
}
}
return time.Time{}
}