github.com/pachyderm/pachyderm@v1.13.4/src/client/auth/auth.go (about)

     1  package auth
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/pachyderm/pachyderm/src/client/pkg/errors"
    10  	"google.golang.org/grpc/codes"
    11  	"google.golang.org/grpc/metadata"
    12  	"google.golang.org/grpc/status"
    13  )
    14  
    15  const (
    16  	// ContextTokenKey is the key of the auth token in an
    17  	// authenticated context
    18  	ContextTokenKey = "authn-token"
    19  
    20  	// The following constants are Subject prefixes. These are prepended to
    21  	// Subjects in the 'tokens' collection, and Principals in 'admins' and on ACLs
    22  	// to indicate what type of Subject or Principal they are (every Pachyderm
    23  	// Subject has a logical Principal with the same name).
    24  
    25  	// GitHubPrefix indicates that this Subject is a GitHub user (because users
    26  	// can authenticate via GitHub, and Pachyderm doesn't have a users table,
    27  	// every GitHub user is also a logical Pachyderm user (but most won't be on
    28  	// any ACLs)
    29  	GitHubPrefix = "github:"
    30  
    31  	// RobotPrefix indicates that this Subject is a Pachyderm robot user. Any
    32  	// string (with this prefix) is a logical Pachyderm robot user.
    33  	RobotPrefix = "robot:"
    34  
    35  	// PipelinePrefix indicates that this Subject is a PPS pipeline. Any string
    36  	// (with this prefix) is a logical PPS pipeline (even though the pipeline may
    37  	// not exist).
    38  	PipelinePrefix = "pipeline:"
    39  
    40  	// PachPrefix indicates that this Subject is an internal Pachyderm user.
    41  	PachPrefix = "pach:"
    42  
    43  	// RootUser is the user created when auth is initialized. Only one token
    44  	// can be created for this user (during auth activation) and they cannot
    45  	// be removed from the set of cluster super-admins.
    46  	RootUser = "pach:root"
    47  )
    48  
    49  // ParseScope parses the string 's' to a scope (for example, parsing a command-
    50  // line argument.
    51  func ParseScope(s string) (Scope, error) {
    52  	for name, value := range Scope_value {
    53  		if strings.EqualFold(s, name) {
    54  			return Scope(value), nil
    55  		}
    56  	}
    57  	return Scope_NONE, errors.Errorf("unrecognized scope: %s", s)
    58  }
    59  
    60  var (
    61  	// ErrNotActivated is returned by an Auth API if the Auth service
    62  	// has not been activated.
    63  	//
    64  	// Note: This error message string is matched in the UI. If edited,
    65  	// it also needs to be updated in the UI code
    66  	ErrNotActivated = status.Error(codes.Unimplemented, "the auth service is not activated")
    67  
    68  	// ErrPartiallyActivated is returned by the auth API to indicated that it's
    69  	// in an intermediate state (in this state, users can retry Activate() or
    70  	// revert with Deactivate(), but not much else)
    71  	ErrPartiallyActivated = status.Error(codes.Unavailable, "the auth service is only partially activated")
    72  
    73  	// ErrNotSignedIn indicates that the caller isn't signed in
    74  	//
    75  	// Note: This error message string is matched in the UI. If edited,
    76  	// it also needs to be updated in the UI code
    77  	ErrNotSignedIn = status.Error(codes.Unauthenticated, "no authentication token (try logging in)")
    78  
    79  	// ErrNoMetadata is returned by the Auth API if the caller sent a request
    80  	// containing no auth token.
    81  	ErrNoMetadata = status.Error(codes.Internal, "no authentication metadata (try logging in)")
    82  
    83  	// ErrBadToken is returned by the Auth API if the caller's token is corrupted
    84  	// or has expired.
    85  	ErrBadToken = status.Error(codes.Unauthenticated, "provided auth token is corrupted or has expired (try logging in again)")
    86  
    87  	// ErrExpiredToken is returned by the Auth API if a restored token expired in
    88  	// the past.
    89  	ErrExpiredToken = status.Error(codes.Internal, "token expiration is in the past")
    90  
    91  	// ErrRevokeUnknownToken is returned by the Auth API if a token to be revoked doesn't exist
    92  	ErrRevokeUnknownToken = status.Error(codes.Internal, "token has expired or is already revoked")
    93  )
    94  
    95  // IsErrNotActivated checks if an error is a ErrNotActivated
    96  func IsErrNotActivated(err error) bool {
    97  	if err == nil {
    98  		return false
    99  	}
   100  	// TODO(msteffen) This is unstructured because we have no way to propagate
   101  	// structured errors across GRPC boundaries. Fix
   102  	return strings.Contains(err.Error(), status.Convert(ErrNotActivated).Message())
   103  }
   104  
   105  // IsErrRevokeUnknownToken checks if an error is a ErrRevokeUnknownToken
   106  func IsErrRevokeUnknownToken(err error) bool {
   107  	if err == nil {
   108  		return false
   109  	}
   110  	// TODO(msteffen) This is unstructured because we have no way to propagate
   111  	// structured errors across GRPC boundaries. Fix
   112  	return strings.Contains(err.Error(), status.Convert(ErrRevokeUnknownToken).Message())
   113  }
   114  
   115  // IsErrPartiallyActivated checks if an error is a ErrPartiallyActivated
   116  func IsErrPartiallyActivated(err error) bool {
   117  	if err == nil {
   118  		return false
   119  	}
   120  	// TODO(msteffen) This is unstructured because we have no way to propagate
   121  	// structured errors across GRPC boundaries. Fix
   122  	return strings.Contains(err.Error(), status.Convert(ErrPartiallyActivated).Message())
   123  }
   124  
   125  // IsErrNotSignedIn returns true if 'err' is a ErrNotSignedIn
   126  func IsErrNotSignedIn(err error) bool {
   127  	if err == nil {
   128  		return false
   129  	}
   130  	// TODO(msteffen) This is unstructured because we have no way to propagate
   131  	// structured errors across GRPC boundaries. Fix
   132  	return strings.Contains(err.Error(), status.Convert(ErrNotSignedIn).Message())
   133  }
   134  
   135  // IsErrNoMetadata returns true if 'err' is an ErrNoMetadata (uses string
   136  // comparison to work across RPC boundaries)
   137  func IsErrNoMetadata(err error) bool {
   138  	if err == nil {
   139  		return false
   140  	}
   141  	return strings.Contains(err.Error(), status.Convert(ErrNoMetadata).Message())
   142  }
   143  
   144  // IsErrBadToken returns true if 'err' is a ErrBadToken
   145  func IsErrBadToken(err error) bool {
   146  	if err == nil {
   147  		return false
   148  	}
   149  	return strings.Contains(err.Error(), status.Convert(ErrBadToken).Message())
   150  }
   151  
   152  // IsErrExpiredToken returns true if 'err' is a ErrExpiredToken
   153  func IsErrExpiredToken(err error) bool {
   154  	if err == nil {
   155  		return false
   156  	}
   157  	return strings.Contains(err.Error(), status.Convert(ErrExpiredToken).Message())
   158  }
   159  
   160  // ErrNotAuthorized is returned if the user is not authorized to perform
   161  // a certain operation. Either
   162  // 1) the operation is a user operation, in which case 'Repo' and/or 'Required'
   163  // 		should be set (indicating that the user needs 'Required'-level access to
   164  // 		'Repo').
   165  // 2) the operation is an admin-only operation (e.g. DeleteAll), in which case
   166  //    AdminOp should be set
   167  type ErrNotAuthorized struct {
   168  	Subject string // subject trying to perform blocked operation -- always set
   169  
   170  	Repo     string // Repo that the user is attempting to access
   171  	Required Scope  // Caller needs 'Required'-level access to 'Repo'
   172  
   173  	// Group 2:
   174  	// AdminOp indicates an operation that the caller couldn't perform because
   175  	// they're not an admin
   176  	AdminOp string
   177  }
   178  
   179  // This error message string is matched in the UI. If edited,
   180  // it also needs to be updated in the UI code
   181  const errNotAuthorizedMsg = "not authorized to perform this operation"
   182  
   183  func (e *ErrNotAuthorized) Error() string {
   184  	var msg string
   185  	if e.Subject != "" {
   186  		msg += e.Subject + " is "
   187  	}
   188  	msg += errNotAuthorizedMsg
   189  	if e.Repo != "" {
   190  		msg += " on the repo " + e.Repo
   191  	}
   192  	if e.Required != Scope_NONE {
   193  		msg += ", must have at least " + e.Required.String() + " access"
   194  	}
   195  	if e.AdminOp != "" {
   196  		msg += "; must be an admin to call " + e.AdminOp
   197  	}
   198  	return msg
   199  }
   200  
   201  // IsErrNotAuthorized checks if an error is a ErrNotAuthorized
   202  func IsErrNotAuthorized(err error) bool {
   203  	if err == nil {
   204  		return false
   205  	}
   206  	// TODO(msteffen) This is unstructured because we have no way to propagate
   207  	// structured errors across GRPC boundaries. Fix
   208  	return strings.Contains(err.Error(), errNotAuthorizedMsg)
   209  }
   210  
   211  // ErrInvalidPrincipal indicates that a an argument to e.g. GetScope,
   212  // SetScope, or SetACL is invalid
   213  type ErrInvalidPrincipal struct {
   214  	Principal string
   215  }
   216  
   217  func (e *ErrInvalidPrincipal) Error() string {
   218  	return fmt.Sprintf("invalid principal \"%s\"; must start with one of \"pipeline:\", \"github:\", or \"robot:\", or have no \":\"", e.Principal)
   219  }
   220  
   221  // IsErrInvalidPrincipal returns true if 'err' is an ErrInvalidPrincipal
   222  func IsErrInvalidPrincipal(err error) bool {
   223  	if err == nil {
   224  		return false
   225  	}
   226  	return strings.Contains(err.Error(), "invalid principal \"") &&
   227  		strings.Contains(err.Error(), "\"; must start with one of \"pipeline:\", \"github:\", or \"robot:\", or have no \":\"")
   228  }
   229  
   230  // ErrTooShortTTL is returned by the ExtendAuthToken if request.Token already
   231  // has a TTL longer than request.TTL.
   232  type ErrTooShortTTL struct {
   233  	RequestTTL, ExistingTTL int64
   234  }
   235  
   236  const errTooShortTTLMsg = "provided TTL (%d) is shorter than token's existing TTL (%d)"
   237  
   238  func (e ErrTooShortTTL) Error() string {
   239  	return fmt.Sprintf(errTooShortTTLMsg, e.RequestTTL, e.ExistingTTL)
   240  }
   241  
   242  // IsErrTooShortTTL returns true if 'err' is a ErrTooShortTTL
   243  func IsErrTooShortTTL(err error) bool {
   244  	if err == nil {
   245  		return false
   246  	}
   247  	errMsg := err.Error()
   248  	return strings.Contains(errMsg, "provided TTL (") &&
   249  		strings.Contains(errMsg, ") is shorter than token's existing TTL (") &&
   250  		strings.Contains(errMsg, ")")
   251  }
   252  
   253  // HashToken converts a token to a cryptographic hash.
   254  // We don't want to store tokens verbatim in the database, as then whoever
   255  // that has access to the database has access to all tokens.
   256  func HashToken(token string) string {
   257  	sum := sha256.Sum256([]byte(token))
   258  	return fmt.Sprintf("%x", sum)
   259  }
   260  
   261  // GetAuthToken extracts the auth token embedded in 'ctx', if there is one
   262  func GetAuthToken(ctx context.Context) (string, error) {
   263  	md, ok := metadata.FromIncomingContext(ctx)
   264  	if !ok {
   265  		return "", ErrNoMetadata
   266  	}
   267  	if len(md[ContextTokenKey]) > 1 {
   268  		return "", errors.Errorf("multiple authentication token keys found in context")
   269  	} else if len(md[ContextTokenKey]) == 0 {
   270  		return "", ErrNotSignedIn
   271  	}
   272  	return md[ContextTokenKey][0], nil
   273  }