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 }