// Copyright 2023 Harness, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package oci import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" apiauth "github.com/harness/gitness/app/api/auth" "github.com/harness/gitness/app/api/render" "github.com/harness/gitness/app/api/request" "github.com/harness/gitness/app/auth" "github.com/harness/gitness/app/jwt" "github.com/harness/gitness/app/paths" "github.com/harness/gitness/app/token" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" "github.com/rs/zerolog/log" ) type TokenResponseOCI struct { Token string `json:"token"` } func (h *Handler) GetToken(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, ok := request.AuthSessionFrom(ctx) if !ok || session.Principal == auth.AnonymousPrincipal { returnForbiddenResponse(w, fmt.Errorf("no auth session found")) return } if tokenMetadata, okt := session.Metadata.(*auth.TokenMetadata); okt && tokenMetadata.TokenType != enum.TokenTypePAT { returnForbiddenResponse(w, fmt.Errorf("only personal access token allowed")) return } user, err := h.UserCtrl.FindNoAuth(ctx, session.Principal.UID) if err != nil { returnForbiddenResponse(w, err) return } requestedOciAccess := GetRequestedResourceActions(getScopes(r.URL)) var accessPermissionsList = []jwt.AccessPermissions{} for _, ra := range requestedOciAccess { space, err := h.getSpace(ctx, ra.Name) if err != nil { render.TranslatedUserError(ctx, w, err) log.Ctx(ctx).Warn().Msgf("failed to find space by ref: %v", err) continue } accessPermissionsList = h.getAccessPermissionList(ctx, space, ra, session, accessPermissionsList) } subClaimsAccessPermissions := &jwt.SubClaimsAccessPermissions{ Source: jwt.OciSource, Permissions: accessPermissionsList, } jwtToken, err := h.getTokenDetails(user, subClaimsAccessPermissions) if err != nil { returnForbiddenResponse(w, err) return } if jwtToken != "" { w.WriteHeader(http.StatusOK) enc := json.NewEncoder(w) if err := enc.Encode( TokenResponseOCI{ Token: jwtToken, }, ); err != nil { log.Error().Msgf("failed to write token response: %v", err) } return } } func (h *Handler) getSpace(ctx context.Context, name string) (*types.Space, error) { spaceRef, _, _ := paths.DisectRoot(name) space, err := h.SpaceStore.FindByRef(ctx, spaceRef) return space, err } func (h *Handler) getAccessPermissionList( ctx context.Context, space *types.Space, ra *ResourceActions, session *auth.Session, accessPermissionsList []jwt.AccessPermissions, ) []jwt.AccessPermissions { accessPermissions := &jwt.AccessPermissions{SpaceID: space.ID, Permissions: []enum.Permission{}} for _, a := range ra.Actions { permission, err := getPermissionFromAction(ctx, a) if err != nil { log.Ctx(ctx).Warn().Msgf("failed to get permission from action: %v", err) continue } scopeErr := apiauth.CheckSpaceScope( ctx, h.Authorizer, session, space, enum.ResourceTypeRegistry, permission, ) if scopeErr != nil { log.Ctx(ctx).Warn().Msgf("failed to check space scope: %v", scopeErr) continue } accessPermissions.Permissions = append(accessPermissions.Permissions, permission) } accessPermissionsList = append(accessPermissionsList, *accessPermissions) return accessPermissionsList } func getPermissionFromAction(ctx context.Context, action string) (enum.Permission, error) { switch action { case "pull": return enum.PermissionArtifactsDownload, nil case "push": return enum.PermissionArtifactsUpload, nil case "delete": return enum.PermissionArtifactsDelete, nil default: err := fmt.Errorf("unknown action: %s", action) log.Ctx(ctx).Err(err).Msgf("Failed to get permission from action: %v", err) return "", err } } func returnForbiddenResponse(w http.ResponseWriter, err error) { w.WriteHeader(http.StatusForbidden) _, err2 := w.Write([]byte(fmt.Sprintf("requested access to the resource is denied: %v", err))) if err2 != nil { log.Error().Msgf("failed to write token response: %v", err2) } } /* * getTokenDetails attempts to get token details. */ func (h *Handler) getTokenDetails( user *types.User, accessPermissions *jwt.SubClaimsAccessPermissions, ) (string, error) { return token.CreateUserWithAccessPermissions(user, accessPermissions) } // GetRequestedResourceActions ... func GetRequestedResourceActions(scopes []string) []*ResourceActions { var res []*ResourceActions for _, s := range scopes { if s == "" { continue } items := strings.Split(s, ":") length := len(items) var resourceType string var resourceName string actions := make([]string, 0) switch length { case 1: resourceType = items[0] case 2: resourceType = items[0] resourceName = items[1] default: resourceType = items[0] resourceName = strings.Join(items[1:length-1], ":") if len(items[length-1]) > 0 { actions = strings.Split(items[length-1], ",") } } res = append( res, &ResourceActions{ Type: resourceType, Name: resourceName, Actions: actions, }, ) } return res } func getScopes(u *url.URL) []string { var sector string var result []string for _, sector = range u.Query()["scope"] { result = append(result, strings.Split(sector, " ")...) } return result } // ResourceActions stores allowed actions on a resource. type ResourceActions struct { Type string `json:"type"` Name string `json:"name"` Actions []string `json:"actions"` }