github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/cmd/authn/tok/token.go (about)

     1  // Package tok provides AuthN token (structure and methods)
     2  // for validation by AIS gateways
     3  /*
     4   * Copyright (c) 2018-2022, NVIDIA CORPORATION. All rights reserved.
     5   */
     6  package tok
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"net/http"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/NVIDIA/aistore/api/apc"
    16  	"github.com/NVIDIA/aistore/api/authn"
    17  	"github.com/NVIDIA/aistore/cmn"
    18  	"github.com/NVIDIA/aistore/cmn/cos"
    19  	"github.com/golang-jwt/jwt/v4"
    20  )
    21  
    22  type Token struct {
    23  	UserID      string          `json:"username"`
    24  	Expires     time.Time       `json:"expires"`
    25  	Token       string          `json:"token"`
    26  	ClusterACLs []*authn.CluACL `json:"clusters"`
    27  	BucketACLs  []*authn.BckACL `json:"buckets,omitempty"`
    28  	IsAdmin     bool            `json:"admin"`
    29  }
    30  
    31  var (
    32  	ErrNoPermissions = errors.New("insufficient permissions")
    33  	ErrInvalidToken  = errors.New("invalid token")
    34  	ErrNoToken       = errors.New("token required")
    35  	ErrNoBearerToken = errors.New("invalid token: no bearer")
    36  	ErrTokenExpired  = errors.New("token expired")
    37  	ErrTokenRevoked  = errors.New("token revoked")
    38  )
    39  
    40  func IssueAdminJWT(expires time.Time, userID, secret string) (string, error) {
    41  	t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    42  		"expires":  expires,
    43  		"username": userID,
    44  		"admin":    true,
    45  	})
    46  	return t.SignedString([]byte(secret))
    47  }
    48  
    49  func IssueJWT(expires time.Time, userID string, bucketACLs []*authn.BckACL, clusterACLs []*authn.CluACL,
    50  	secret string) (string, error) {
    51  	t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    52  		"expires":  expires,
    53  		"username": userID,
    54  		"buckets":  bucketACLs,
    55  		"clusters": clusterACLs,
    56  	})
    57  	return t.SignedString([]byte(secret))
    58  }
    59  
    60  // Header format: 'Authorization: Bearer <token>'
    61  func ExtractToken(hdr http.Header) (string, error) {
    62  	s := hdr.Get(apc.HdrAuthorization)
    63  	if s == "" {
    64  		return "", ErrNoToken
    65  	}
    66  	idx := strings.Index(s, " ")
    67  	if idx == -1 || s[:idx] != apc.AuthenticationTypeBearer {
    68  		return "", ErrNoBearerToken
    69  	}
    70  	return s[idx+1:], nil
    71  }
    72  
    73  func DecryptToken(tokenStr, secret string) (*Token, error) {
    74  	jwtToken, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
    75  		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
    76  			return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
    77  		}
    78  		return []byte(secret), nil
    79  	})
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	claims, ok := jwtToken.Claims.(jwt.MapClaims)
    84  	if !ok || !jwtToken.Valid {
    85  		return nil, ErrInvalidToken
    86  	}
    87  	tk := &Token{}
    88  	if err := cos.MorphMarshal(claims, tk); err != nil {
    89  		return nil, ErrInvalidToken
    90  	}
    91  	return tk, nil
    92  }
    93  
    94  ///////////
    95  // Token //
    96  ///////////
    97  
    98  func (tk *Token) String() string {
    99  	return fmt.Sprintf("user %s, %s", tk.UserID, expiresIn(tk.Expires))
   100  }
   101  
   102  // A user has two-level permissions: cluster-wide and on per bucket basis.
   103  // To be able to access data, a user must have either permission. This
   104  // allows creating users, e.g, with read-only access to the entire cluster,
   105  // and read-write access to a single bucket.
   106  // Per-bucket ACL overrides cluster-wide one.
   107  // Permissions for a cluster with empty ID are used as default ones when
   108  // a user do not have permissions for the given `clusterID`.
   109  //
   110  // ACL rules are checked in the following order (from highest to the lowest priority):
   111  //  1. A user's role is an admin.
   112  //  2. User's permissions for the given bucket
   113  //  3. User's permissions for the given cluster
   114  //  4. User's default cluster permissions (ACL for a cluster with empty clusterID)
   115  //
   116  // If there are no defined ACL found at any step, any access is denied.
   117  func (tk *Token) CheckPermissions(clusterID string, bck *cmn.Bck, perms apc.AccessAttrs) error {
   118  	if tk.IsAdmin {
   119  		return nil
   120  	}
   121  	if perms == 0 {
   122  		return errors.New("empty permissions requested")
   123  	}
   124  	cluPerms := perms & apc.AccessCluster
   125  	objPerms := perms &^ apc.AccessCluster
   126  	cluACL, cluOk := tk.aclForCluster(clusterID)
   127  	if cluPerms != 0 {
   128  		// Cluster-wide permissions requested
   129  		if !cluOk {
   130  			return ErrNoPermissions
   131  		}
   132  		if clusterID == "" {
   133  			return errors.New("requested cluster permissions without cluster ID")
   134  		}
   135  		if !cluACL.Has(cluPerms) {
   136  			return fmt.Errorf("%v: [cluster %s, %s, granted(%s)]",
   137  				ErrNoPermissions, clusterID, tk, cluACL.Describe(false /*include all*/))
   138  		}
   139  	}
   140  	if objPerms == 0 {
   141  		return nil
   142  	}
   143  
   144  	// Check only bucket specific permissions.
   145  	if bck == nil {
   146  		return errors.New("requested bucket permissions without a bucket")
   147  	}
   148  	bckACL, bckOk := tk.aclForBucket(clusterID, bck)
   149  	if bckOk {
   150  		if bckACL.Has(objPerms) {
   151  			return nil
   152  		}
   153  		return fmt.Errorf("%v: [%s, bucket %s, granted(%s)]",
   154  			ErrNoPermissions, tk, bck.String(), bckACL.Describe(false /*include all*/))
   155  	}
   156  	if !cluOk || !cluACL.Has(objPerms) {
   157  		return fmt.Errorf("%v: [%s, granted(%s)]", ErrNoPermissions, tk, cluACL.Describe(false /*include all*/))
   158  	}
   159  	return nil
   160  }
   161  
   162  //
   163  // private
   164  //
   165  
   166  func expiresIn(tm time.Time) string {
   167  	now := time.Now()
   168  	if now.After(tm) {
   169  		return ErrTokenExpired.Error()
   170  	}
   171  	// round up
   172  	d := tm.Sub(now) / time.Second
   173  	d *= time.Second
   174  	return "token expires in " + d.String()
   175  }
   176  
   177  func (tk *Token) aclForCluster(clusterID string) (perms apc.AccessAttrs, ok bool) {
   178  	var defaultCluster *authn.CluACL
   179  	for _, pm := range tk.ClusterACLs {
   180  		if pm.ID == clusterID {
   181  			return pm.Access, true
   182  		}
   183  		if pm.ID == "" {
   184  			defaultCluster = pm
   185  		}
   186  	}
   187  	if defaultCluster != nil {
   188  		return defaultCluster.Access, true
   189  	}
   190  	return 0, false
   191  }
   192  
   193  func (tk *Token) aclForBucket(clusterID string, bck *cmn.Bck) (perms apc.AccessAttrs, ok bool) {
   194  	for _, b := range tk.BucketACLs {
   195  		tbBck := b.Bck
   196  		if tbBck.Ns.UUID != clusterID {
   197  			continue
   198  		}
   199  		// For AuthN all buckets are external: they have UUIDs of the respective AIS clusters.
   200  		// To correctly compare with the caller's `bck` we construct tokenBck from the token.
   201  		tokenBck := cmn.Bck{Name: tbBck.Name, Provider: tbBck.Provider}
   202  		if tokenBck.Equal(bck) {
   203  			return b.Access, true
   204  		}
   205  	}
   206  	return 0, false
   207  }