// 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 authz import ( "context" "fmt" "github.com/harness/gitness/app/auth" "github.com/harness/gitness/app/paths" "github.com/harness/gitness/app/services/publicaccess" "github.com/harness/gitness/app/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" "github.com/rs/zerolog/log" "golang.org/x/exp/slices" ) var _ Authorizer = (*MembershipAuthorizer)(nil) type MembershipAuthorizer struct { permissionCache PermissionCache spaceStore store.SpaceStore publicAccess publicaccess.Service } func NewMembershipAuthorizer( permissionCache PermissionCache, spaceStore store.SpaceStore, publicAccess publicaccess.Service, ) *MembershipAuthorizer { return &MembershipAuthorizer{ permissionCache: permissionCache, spaceStore: spaceStore, publicAccess: publicAccess, } } func (a *MembershipAuthorizer) Check( ctx context.Context, session *auth.Session, scope *types.Scope, resource *types.Resource, permission enum.Permission, ) (bool, error) { publicAccessAllowed, err := CheckPublicAccess(ctx, a.publicAccess, scope, resource, permission) if err != nil { return false, fmt.Errorf("failed to check public access: %w", err) } if publicAccessAllowed { return true, nil } log.Ctx(ctx).Debug().Msgf( "[MembershipAuthorizer] %s with id '%d' requests %s for %s '%s' in scope %#v with metadata %#v", session.Principal.Type, session.Principal.ID, permission, resource.Type, resource.Identifier, scope, session.Metadata, ) if session.Principal.Admin { return true, nil // system admin can call any API } var spacePath string //nolint:exhaustive // we want to fail on anything else switch resource.Type { case enum.ResourceTypeSpace: spacePath = paths.Concatenate(scope.SpacePath, resource.Identifier) case enum.ResourceTypeRepo: spacePath = scope.SpacePath case enum.ResourceTypeServiceAccount: spacePath = scope.SpacePath case enum.ResourceTypePipeline: spacePath = scope.SpacePath case enum.ResourceTypeSecret: spacePath = scope.SpacePath case enum.ResourceTypeConnector: spacePath = scope.SpacePath case enum.ResourceTypeTemplate: spacePath = scope.SpacePath case enum.ResourceTypeGitspace: spacePath = scope.SpacePath case enum.ResourceTypeInfraProvider: spacePath = scope.SpacePath case enum.ResourceTypeRegistry: spacePath = scope.SpacePath case enum.ResourceTypeUser: // a user is allowed to edit themselves if resource.Identifier == session.Principal.UID && permission == enum.PermissionUserEdit { return true, nil } // user can see all other users in the system. if permission == enum.PermissionUserView { return true, nil } // everything else is reserved for admins only (like operations on users other than yourself, or setting admin) return false, nil // Service operations aren't exposed to users case enum.ResourceTypeService: return false, nil default: return false, nil } // ephemeral membership overrides any other space memberships of the principal if membershipMetadata, ok := session.Metadata.(*auth.MembershipMetadata); ok { return a.checkWithMembershipMetadata(ctx, membershipMetadata, spacePath, permission) } // accessPermissionMetadata contains the access permissions of per space if accessPermissionMetadata, ok := session.Metadata.(*auth.AccessPermissionMetadata); ok { return a.checkWithAccessPermissionMetadata(ctx, accessPermissionMetadata, spacePath, permission) } // ensure we aren't bypassing unknown metadata with impact on authorization if session.Metadata != nil && session.Metadata.ImpactsAuthorization() { return false, fmt.Errorf("session contains unknown metadata that impacts authorization: %T", session.Metadata) } return a.permissionCache.Get( ctx, PermissionCacheKey{ PrincipalID: session.Principal.ID, SpaceRef: spacePath, Permission: permission, }, ) } func (a *MembershipAuthorizer) CheckAll( ctx context.Context, session *auth.Session, permissionChecks ...types.PermissionCheck, ) (bool, error) { for i := range permissionChecks { p := permissionChecks[i] if _, err := a.Check(ctx, session, &p.Scope, &p.Resource, p.Permission); err != nil { return false, err } } return true, nil } // checkWithMembershipMetadata checks access using the ephemeral membership provided in the metadata. func (a *MembershipAuthorizer) checkWithMembershipMetadata( ctx context.Context, membershipMetadata *auth.MembershipMetadata, requestedSpacePath string, requestedPermission enum.Permission, ) (bool, error) { space, err := a.spaceStore.Find(ctx, membershipMetadata.SpaceID) if err != nil { return false, fmt.Errorf("failed to find space: %w", err) } if !paths.IsAncesterOf(space.Path, requestedSpacePath) { return false, fmt.Errorf( "requested permission scope '%s' is outside of ephemeral membership scope '%s'", requestedSpacePath, space.Path, ) } if !roleHasPermission(membershipMetadata.Role, requestedPermission) { return false, fmt.Errorf( "requested permission '%s' is outside of ephemeral membership role '%s'", requestedPermission, membershipMetadata.Role, ) } // access is granted by ephemeral membership return true, nil } // checkWithAccessPermissionMetadata checks access using the ephemeral membership provided in the metadata. func (a *MembershipAuthorizer) checkWithAccessPermissionMetadata( ctx context.Context, accessPermissionMetadata *auth.AccessPermissionMetadata, requestedSpacePath string, requestedPermission enum.Permission, ) (bool, error) { space, err := a.spaceStore.FindByRef(ctx, requestedSpacePath) if err != nil { return false, fmt.Errorf("failed to find space by ref: %w", err) } if accessPermissionMetadata.AccessPermissions.Permissions == nil { return false, fmt.Errorf("no %s permission provided", requestedPermission) } for _, accessPermission := range accessPermissionMetadata.AccessPermissions.Permissions { if space.ID == accessPermission.SpaceID && slices.Contains(accessPermission.Permissions, requestedPermission) { return true, nil } } return false, fmt.Errorf("no %s permission provided", requestedPermission) }