mirror of https://github.com/harness/drone.git
[Standalone] Add temporary JWT for pipeline executions (#480)
parent
fedd04a2da
commit
8cd3e5d015
|
@ -13,7 +13,6 @@ import (
|
|||
|
||||
"github.com/harness/gitness/cli/provide"
|
||||
"github.com/harness/gitness/internal/api/controller/user"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/drone/funcmap"
|
||||
"github.com/gotidy/ptr"
|
||||
|
@ -47,7 +46,6 @@ func (c *createPATCommand) run(*kingpin.ParseContext) error {
|
|||
in := user.CreateTokenInput{
|
||||
UID: c.uid,
|
||||
Lifetime: lifeTime,
|
||||
Grants: enum.AccessGrantAll,
|
||||
}
|
||||
|
||||
tokenResp, err := provide.Client().UserCreatePAT(ctx, in)
|
||||
|
|
|
@ -85,7 +85,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||
principalInfoCache := cache.ProvidePrincipalInfoCache(principalInfoView)
|
||||
membershipStore := database.ProvideMembershipStore(db, principalInfoCache)
|
||||
permissionCache := authz.ProvidePermissionCache(spaceStore, membershipStore)
|
||||
authorizer := authz.ProvideAuthorizer(permissionCache)
|
||||
authorizer := authz.ProvideAuthorizer(permissionCache, spaceStore)
|
||||
principalUIDTransformation := store.ProvidePrincipalUIDTransformation()
|
||||
principalStore := database.ProvidePrincipalStore(db, principalUIDTransformation)
|
||||
tokenStore := database.ProvideTokenStore(db)
|
||||
|
|
|
@ -17,14 +17,17 @@ import (
|
|||
)
|
||||
|
||||
type CreateTokenInput struct {
|
||||
UID string `json:"uid"`
|
||||
Lifetime *time.Duration `json:"lifetime"`
|
||||
Grants enum.AccessGrant `json:"grants"`
|
||||
UID string `json:"uid"`
|
||||
Lifetime *time.Duration `json:"lifetime"`
|
||||
}
|
||||
|
||||
// CreateToken creates a new service account access token.
|
||||
func (c *Controller) CreateToken(ctx context.Context, session *auth.Session,
|
||||
saUID string, in *CreateTokenInput) (*types.TokenResponse, error) {
|
||||
func (c *Controller) CreateToken(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
saUID string,
|
||||
in *CreateTokenInput,
|
||||
) (*types.TokenResponse, error) {
|
||||
sa, err := findServiceAccountFromUID(ctx, c.principalStore, saUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -36,18 +39,20 @@ func (c *Controller) CreateToken(ctx context.Context, session *auth.Session,
|
|||
if err = check.TokenLifetime(in.Lifetime, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: Added to unblock UI - Depending on product decision enforce grants, or remove Grants completely.
|
||||
if err = check.AccessGrant(in.Grants, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure principal has required permissions on parent (ensures that parent exists)
|
||||
if err = apiauth.CheckServiceAccount(ctx, c.authorizer, session, c.spaceStore, c.repoStore,
|
||||
sa.ParentType, sa.ParentID, sa.UID, enum.PermissionServiceAccountEdit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, jwtToken, err := token.CreateSAT(ctx, c.tokenStore, &session.Principal,
|
||||
sa, in.UID, in.Lifetime, in.Grants)
|
||||
token, jwtToken, err := token.CreateSAT(
|
||||
ctx,
|
||||
c.tokenStore,
|
||||
&session.Principal,
|
||||
sa,
|
||||
in.UID,
|
||||
in.Lifetime,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -17,16 +17,19 @@ import (
|
|||
)
|
||||
|
||||
type CreateTokenInput struct {
|
||||
UID string `json:"uid"`
|
||||
Lifetime *time.Duration `json:"lifetime"`
|
||||
Grants enum.AccessGrant `json:"grants"`
|
||||
UID string `json:"uid"`
|
||||
Lifetime *time.Duration `json:"lifetime"`
|
||||
}
|
||||
|
||||
/*
|
||||
* CreateToken creates a new user access token.
|
||||
*/
|
||||
func (c *Controller) CreateAccessToken(ctx context.Context, session *auth.Session,
|
||||
userUID string, in *CreateTokenInput) (*types.TokenResponse, error) {
|
||||
func (c *Controller) CreateAccessToken(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
userUID string,
|
||||
in *CreateTokenInput,
|
||||
) (*types.TokenResponse, error) {
|
||||
user, err := findUserFromUID(ctx, c.principalStore, userUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -43,13 +46,15 @@ func (c *Controller) CreateAccessToken(ctx context.Context, session *auth.Sessio
|
|||
if err = check.TokenLifetime(in.Lifetime, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: Added to unblock UI - Depending on product decision enforce grants, or remove Grants completely.
|
||||
if err = check.AccessGrant(in.Grants, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, jwtToken, err := token.CreatePAT(ctx, c.tokenStore, &session.Principal,
|
||||
user, in.UID, in.Lifetime, in.Grants)
|
||||
token, jwtToken, err := token.CreatePAT(
|
||||
ctx,
|
||||
c.tokenStore,
|
||||
&session.Principal,
|
||||
user,
|
||||
in.UID,
|
||||
in.Lifetime,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ func (c *Controller) Logout(ctx context.Context, session *auth.Session) error {
|
|||
tokenID = t.TokenID
|
||||
tokenType = t.TokenType
|
||||
default:
|
||||
return errors.New("session metadata is of unknown type")
|
||||
return errors.New("provided jwt doesn't support logout")
|
||||
}
|
||||
|
||||
if tokenType != enum.TokenTypeSession {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package authn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -12,34 +13,31 @@ import (
|
|||
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/internal/jwt"
|
||||
"github.com/harness/gitness/internal/store"
|
||||
"github.com/harness/gitness/internal/token"
|
||||
"github.com/harness/gitness/types"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
gojwt "github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
var _ Authenticator = (*TokenAuthenticator)(nil)
|
||||
var _ Authenticator = (*JWTAuthenticator)(nil)
|
||||
|
||||
/*
|
||||
* Authenticates a user by checking for an access token in the
|
||||
* "Authorization" header or the "access_token" form value.
|
||||
*/
|
||||
type TokenAuthenticator struct {
|
||||
// JWTAuthenticator uses the provided JWT to authenticate the caller.
|
||||
type JWTAuthenticator struct {
|
||||
principalStore store.PrincipalStore
|
||||
tokenStore store.TokenStore
|
||||
}
|
||||
|
||||
func NewTokenAuthenticator(
|
||||
principalStore store.PrincipalStore,
|
||||
tokenStore store.TokenStore) *TokenAuthenticator {
|
||||
return &TokenAuthenticator{
|
||||
tokenStore store.TokenStore) *JWTAuthenticator {
|
||||
return &JWTAuthenticator{
|
||||
principalStore: principalStore,
|
||||
tokenStore: tokenStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *TokenAuthenticator) Authenticate(r *http.Request, sourceRouter SourceRouter) (*auth.Session, error) {
|
||||
func (a *JWTAuthenticator) Authenticate(r *http.Request, sourceRouter SourceRouter) (*auth.Session, error) {
|
||||
ctx := r.Context()
|
||||
str := extractToken(r)
|
||||
|
||||
|
@ -49,8 +47,8 @@ func (a *TokenAuthenticator) Authenticate(r *http.Request, sourceRouter SourceRo
|
|||
|
||||
var principal *types.Principal
|
||||
var err error
|
||||
claims := &token.JWTClaims{}
|
||||
parsed, err := jwt.ParseWithClaims(str, claims, func(token_ *jwt.Token) (interface{}, error) {
|
||||
claims := &jwt.Claims{}
|
||||
parsed, err := gojwt.ParseWithClaims(str, claims, func(token_ *gojwt.Token) (interface{}, error) {
|
||||
principal, err = a.principalStore.Find(ctx, claims.PrincipalID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get principal for token: %w", err)
|
||||
|
@ -65,12 +63,39 @@ func (a *TokenAuthenticator) Authenticate(r *http.Request, sourceRouter SourceRo
|
|||
return nil, errors.New("parsed JWT token is invalid")
|
||||
}
|
||||
|
||||
if _, ok := parsed.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
if _, ok := parsed.Method.(*gojwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("invalid HMAC signature for JWT")
|
||||
}
|
||||
|
||||
var metadata auth.Metadata
|
||||
switch {
|
||||
case claims.Token != nil:
|
||||
metadata, err = a.metadataFromTokenClaims(ctx, principal, claims.Token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get metadata from token claims: %w", err)
|
||||
}
|
||||
case claims.Membership != nil:
|
||||
metadata, err = a.metadataFromMembershipClaims(claims.Membership)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get metadata from membership claims: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("jwt is missing sub-claims")
|
||||
}
|
||||
|
||||
return &auth.Session{
|
||||
Principal: *principal,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *JWTAuthenticator) metadataFromTokenClaims(
|
||||
ctx context.Context,
|
||||
principal *types.Principal,
|
||||
tknClaims *jwt.SubClaimsToken,
|
||||
) (auth.Metadata, error) {
|
||||
// ensure tkn exists
|
||||
tkn, err := a.tokenStore.Find(ctx, claims.TokenID)
|
||||
tkn, err := a.tokenStore.Find(ctx, tknClaims.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find token in db: %w", err)
|
||||
}
|
||||
|
@ -81,13 +106,19 @@ func (a *TokenAuthenticator) Authenticate(r *http.Request, sourceRouter SourceRo
|
|||
principal.ID, tkn.PrincipalID)
|
||||
}
|
||||
|
||||
return &auth.Session{
|
||||
Principal: *principal,
|
||||
Metadata: &auth.TokenMetadata{
|
||||
TokenType: tkn.Type,
|
||||
TokenID: tkn.ID,
|
||||
Grants: tkn.Grants,
|
||||
},
|
||||
return &auth.TokenMetadata{
|
||||
TokenType: tkn.Type,
|
||||
TokenID: tkn.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *JWTAuthenticator) metadataFromMembershipClaims(
|
||||
mbsClaims *jwt.SubClaimsMembership,
|
||||
) (auth.Metadata, error) {
|
||||
// We could check if space exists - but also okay to fail later (saves db call)
|
||||
return &auth.MembershipMetadata{
|
||||
SpaceID: mbsClaims.SpaceID,
|
||||
Role: mbsClaims.Role,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -6,9 +6,11 @@ package authz
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/internal/paths"
|
||||
"github.com/harness/gitness/internal/store"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
|
@ -19,13 +21,16 @@ var _ Authorizer = (*MembershipAuthorizer)(nil)
|
|||
|
||||
type MembershipAuthorizer struct {
|
||||
permissionCache PermissionCache
|
||||
spaceStore store.SpaceStore
|
||||
}
|
||||
|
||||
func NewMembershipAuthorizer(
|
||||
permissionCache PermissionCache,
|
||||
spaceStore store.SpaceStore,
|
||||
) *MembershipAuthorizer {
|
||||
return &MembershipAuthorizer{
|
||||
permissionCache: permissionCache,
|
||||
spaceStore: spaceStore,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,23 +56,23 @@ func (a *MembershipAuthorizer) Check(
|
|||
return true, nil // system admin can call any API
|
||||
}
|
||||
|
||||
var spaceRef string
|
||||
var spacePath string
|
||||
|
||||
switch resource.Type {
|
||||
case enum.ResourceTypeSpace:
|
||||
spaceRef = paths.Concatinate(scope.SpacePath, resource.Name)
|
||||
spacePath = paths.Concatinate(scope.SpacePath, resource.Name)
|
||||
|
||||
case enum.ResourceTypeRepo:
|
||||
spaceRef = scope.SpacePath
|
||||
spacePath = scope.SpacePath
|
||||
|
||||
case enum.ResourceTypeServiceAccount:
|
||||
spaceRef = scope.SpacePath
|
||||
spacePath = scope.SpacePath
|
||||
|
||||
case enum.ResourceTypePipeline:
|
||||
spaceRef = scope.SpacePath
|
||||
spacePath = scope.SpacePath
|
||||
|
||||
case enum.ResourceTypeSecret:
|
||||
spaceRef = scope.SpacePath
|
||||
spacePath = scope.SpacePath
|
||||
|
||||
case enum.ResourceTypeUser:
|
||||
// a user is allowed to view / edit themselves
|
||||
|
@ -87,12 +92,23 @@ func (a *MembershipAuthorizer) Check(
|
|||
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)
|
||||
}
|
||||
|
||||
// ensure we aren't bypassing unknown metadata with impact on authorization
|
||||
if 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: spaceRef,
|
||||
SpaceRef: spacePath,
|
||||
Permission: permission,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *MembershipAuthorizer) CheckAll(ctx context.Context, session *auth.Session,
|
||||
permissionChecks ...types.PermissionCheck) (bool, error) {
|
||||
for _, p := range permissionChecks {
|
||||
|
@ -103,3 +119,35 @@ func (a *MembershipAuthorizer) CheckAll(ctx context.Context, session *auth.Sessi
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -67,11 +67,9 @@ func (g permissionCacheGetter) Find(ctx context.Context, key PermissionCacheKey)
|
|||
}
|
||||
|
||||
// If the membership is defined in the current space, check if the user has the required permission.
|
||||
if membership != nil {
|
||||
_, hasRole := slices.BinarySearch(membership.Role.Permissions(), key.Permission)
|
||||
if hasRole {
|
||||
return true, nil
|
||||
}
|
||||
if membership != nil &&
|
||||
roleHasPermission(membership.Role, key.Permission) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If membership with the requested permission has not been found in the current space,
|
||||
|
@ -89,3 +87,8 @@ func (g permissionCacheGetter) Find(ctx context.Context, key PermissionCacheKey)
|
|||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func roleHasPermission(role enum.MembershipRole, permission enum.Permission) bool {
|
||||
_, hasRole := slices.BinarySearch(role.Permissions(), permission)
|
||||
return hasRole
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ var WireSet = wire.NewSet(
|
|||
ProvidePermissionCache,
|
||||
)
|
||||
|
||||
func ProvideAuthorizer(pCache PermissionCache) Authorizer {
|
||||
return NewMembershipAuthorizer(pCache)
|
||||
func ProvideAuthorizer(pCache PermissionCache, spaceStore store.SpaceStore) Authorizer {
|
||||
return NewMembershipAuthorizer(pCache, spaceStore)
|
||||
}
|
||||
|
||||
func ProvidePermissionCache(
|
||||
|
|
|
@ -17,23 +17,22 @@ func (m *EmptyMetadata) ImpactsAuthorization() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// SSHMetadata contains information about the ssh connection that was used during auth.
|
||||
type SSHMetadata struct {
|
||||
KeyID string
|
||||
Grants enum.AccessGrant // retrieved from ssh key table during verification
|
||||
}
|
||||
|
||||
func (m *SSHMetadata) ImpactsAuthorization() bool {
|
||||
return m.Grants != enum.AccessGrantAll
|
||||
}
|
||||
|
||||
// TokenMetadata contains information about the token that was used during auth.
|
||||
type TokenMetadata struct {
|
||||
TokenType enum.TokenType
|
||||
TokenID int64
|
||||
Grants enum.AccessGrant // retrieved from token during verification
|
||||
}
|
||||
|
||||
func (m *TokenMetadata) ImpactsAuthorization() bool {
|
||||
return m.Grants != enum.AccessGrantAll
|
||||
return false
|
||||
}
|
||||
|
||||
// MembershipMetadata contains information about an ephemeral membership grant.
|
||||
type MembershipMetadata struct {
|
||||
SpaceID int64
|
||||
Role enum.MembershipRole
|
||||
}
|
||||
|
||||
func (m *MembershipMetadata) ImpactsAuthorization() bool {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -29,6 +29,17 @@ func NewSystemServiceSession() *auth.Session {
|
|||
}
|
||||
}
|
||||
|
||||
// pipelineServicePrincipal is the principal that is used during
|
||||
// pipeline executions for calling gitness APIs.
|
||||
var pipelineServicePrincipal *types.Principal
|
||||
|
||||
func NewPipelineServiceSession() *auth.Session {
|
||||
return &auth.Session{
|
||||
Principal: *pipelineServicePrincipal,
|
||||
Metadata: &auth.EmptyMetadata{},
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap is an abstraction of a function that bootstraps a system.
|
||||
type Bootstrap func(context.Context) error
|
||||
|
||||
|
@ -36,11 +47,15 @@ func System(config *types.Config, userCtrl *user.Controller,
|
|||
serviceCtrl *service.Controller) func(context.Context) error {
|
||||
return func(ctx context.Context) error {
|
||||
if err := SystemService(ctx, config, serviceCtrl); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to setup system service: %w", err)
|
||||
}
|
||||
|
||||
if err := PipelineService(ctx, config, serviceCtrl); err != nil {
|
||||
return fmt.Errorf("failed to setup pipeline service: %w", err)
|
||||
}
|
||||
|
||||
if err := AdminUser(ctx, config, userCtrl); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to setup admin user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -70,7 +85,11 @@ func AdminUser(ctx context.Context, config *types.Config, userCtrl *user.Control
|
|||
return nil
|
||||
}
|
||||
|
||||
func createAdminUser(ctx context.Context, config *types.Config, userCtrl *user.Controller) (*types.User, error) {
|
||||
func createAdminUser(
|
||||
ctx context.Context,
|
||||
config *types.Config,
|
||||
userCtrl *user.Controller,
|
||||
) (*types.User, error) {
|
||||
in := &user.CreateInput{
|
||||
UID: config.Principal.Admin.UID,
|
||||
DisplayName: config.Principal.Admin.DisplayName,
|
||||
|
@ -96,10 +115,21 @@ func createAdminUser(ctx context.Context, config *types.Config, userCtrl *user.C
|
|||
|
||||
// SystemService sets up the gitness service principal that is used for
|
||||
// resources that are automatically created by the system.
|
||||
func SystemService(ctx context.Context, config *types.Config, serviceCtrl *service.Controller) error {
|
||||
func SystemService(
|
||||
ctx context.Context,
|
||||
config *types.Config,
|
||||
serviceCtrl *service.Controller,
|
||||
) error {
|
||||
svc, err := serviceCtrl.FindNoAuth(ctx, config.Principal.System.UID)
|
||||
if errors.Is(err, store.ErrResourceNotFound) {
|
||||
svc, err = createSystemService(ctx, config, serviceCtrl)
|
||||
svc, err = createServicePrincipal(
|
||||
ctx,
|
||||
serviceCtrl,
|
||||
config.Principal.System.UID,
|
||||
config.Principal.System.Email,
|
||||
config.Principal.System.DisplayName,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -116,25 +146,65 @@ func SystemService(ctx context.Context, config *types.Config, serviceCtrl *servi
|
|||
return nil
|
||||
}
|
||||
|
||||
func createSystemService(ctx context.Context, config *types.Config,
|
||||
serviceCtrl *service.Controller) (*types.Service, error) {
|
||||
in := &service.CreateInput{
|
||||
UID: config.Principal.System.UID,
|
||||
Email: config.Principal.System.Email,
|
||||
DisplayName: config.Principal.System.DisplayName,
|
||||
// PipelineService sets up the pipeline service principal that is used during
|
||||
// pipeline executions for calling gitness APIs.
|
||||
func PipelineService(
|
||||
ctx context.Context,
|
||||
config *types.Config,
|
||||
serviceCtrl *service.Controller,
|
||||
) error {
|
||||
svc, err := serviceCtrl.FindNoAuth(ctx, config.Principal.Pipeline.UID)
|
||||
if errors.Is(err, store.ErrResourceNotFound) {
|
||||
svc, err = createServicePrincipal(
|
||||
ctx,
|
||||
serviceCtrl,
|
||||
config.Principal.Pipeline.UID,
|
||||
config.Principal.Pipeline.Email,
|
||||
config.Principal.Pipeline.DisplayName,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
svc, createErr := serviceCtrl.CreateNoAuth(ctx, in, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup pipeline service: %w", err)
|
||||
}
|
||||
|
||||
pipelineServicePrincipal = svc.ToPrincipal()
|
||||
|
||||
log.Ctx(ctx).Info().Msgf("Completed setup of pipeline service '%s' (id: %d).", svc.UID, svc.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createServicePrincipal(
|
||||
ctx context.Context,
|
||||
serviceCtrl *service.Controller,
|
||||
uid string,
|
||||
email string,
|
||||
displayName string,
|
||||
admin bool,
|
||||
) (*types.Service, error) {
|
||||
in := &service.CreateInput{
|
||||
UID: uid,
|
||||
Email: email,
|
||||
DisplayName: displayName,
|
||||
}
|
||||
|
||||
svc, createErr := serviceCtrl.CreateNoAuth(ctx, in, admin)
|
||||
if createErr == nil || !errors.Is(createErr, store.ErrDuplicate) {
|
||||
return svc, createErr
|
||||
}
|
||||
|
||||
// service might've been created by another instance in which case we should find it now.
|
||||
var findErr error
|
||||
svc, findErr = serviceCtrl.FindNoAuth(ctx, config.Principal.System.UID)
|
||||
svc, findErr = serviceCtrl.FindNoAuth(ctx, uid)
|
||||
if findErr != nil {
|
||||
return nil, fmt.Errorf("failed to find service with uid '%s' (%s) after duplicate error: %w",
|
||||
config.Principal.System.UID, findErr, createErr)
|
||||
return nil, fmt.Errorf(
|
||||
"failed to find service with uid '%s' (%s) after duplicate error: %w",
|
||||
uid,
|
||||
findErr,
|
||||
createErr,
|
||||
)
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
issuer = "Gitness"
|
||||
)
|
||||
|
||||
// Claims defines gitness jwt claims.
|
||||
type Claims struct {
|
||||
jwt.StandardClaims
|
||||
|
||||
PrincipalID int64 `json:"pid,omitempty"`
|
||||
|
||||
Token *SubClaimsToken `json:"tkn,omitempty"`
|
||||
Membership *SubClaimsMembership `json:"ms,omitempty"`
|
||||
}
|
||||
|
||||
// SubClaimsToken contains information about the token the JWT was created for.
|
||||
type SubClaimsToken struct {
|
||||
Type enum.TokenType `json:"typ,omitempty"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// SubClaimsMembership contains the ephemeral membership the JWT was created with.
|
||||
type SubClaimsMembership struct {
|
||||
Role enum.MembershipRole `json:"role,omitempty"`
|
||||
SpaceID int64 `json:"sid,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateForToken generates a jwt for a given token.
|
||||
func GenerateForToken(token *types.Token, secret string) (string, error) {
|
||||
var expiresAt int64
|
||||
if token.ExpiresAt != nil {
|
||||
expiresAt = *token.ExpiresAt
|
||||
}
|
||||
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
// times required to be in sec not millisec
|
||||
IssuedAt: token.IssuedAt / 1000,
|
||||
ExpiresAt: expiresAt / 1000,
|
||||
},
|
||||
PrincipalID: token.PrincipalID,
|
||||
Token: &SubClaimsToken{
|
||||
Type: token.Type,
|
||||
ID: token.ID,
|
||||
},
|
||||
})
|
||||
|
||||
res, err := jwtToken.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed to sign token")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GenerateWithMembership generates a jwt with the given ephemeral membership.
|
||||
func GenerateWithMembership(principalID int64, spaceID int64, role enum.MembershipRole, lifetime time.Duration, secret string) (string, error) {
|
||||
issuedAt := time.Now()
|
||||
expiresAt := issuedAt.Add(lifetime)
|
||||
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
// times required to be in sec
|
||||
IssuedAt: issuedAt.Unix(),
|
||||
ExpiresAt: expiresAt.Unix(),
|
||||
},
|
||||
PrincipalID: principalID,
|
||||
Membership: &SubClaimsMembership{
|
||||
SpaceID: spaceID,
|
||||
Role: role,
|
||||
},
|
||||
})
|
||||
|
||||
res, err := jwtToken.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed to sign token")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
|
@ -18,6 +18,8 @@ var (
|
|||
// DisectLeaf splits a path into its parent path and the leaf name
|
||||
// e.g. space1/space2/space3 -> (space1/space2, space3, nil).
|
||||
func DisectLeaf(path string) (string, string, error) {
|
||||
path = strings.Trim(path, types.PathSeparator)
|
||||
|
||||
if path == "" {
|
||||
return "", "", ErrPathEmpty
|
||||
}
|
||||
|
@ -33,6 +35,8 @@ func DisectLeaf(path string) (string, string, error) {
|
|||
// DisectRoot splits a path into its root space and sub-path
|
||||
// e.g. space1/space2/space3 -> (space1, space2/space3, nil).
|
||||
func DisectRoot(path string) (string, string, error) {
|
||||
path = strings.Trim(path, types.PathSeparator)
|
||||
|
||||
if path == "" {
|
||||
return "", "", ErrPathEmpty
|
||||
}
|
||||
|
@ -67,5 +71,19 @@ func Concatinate(path1 string, path2 string) string {
|
|||
// Segments returns all segments of the path
|
||||
// e.g. /space1/space2/space3 -> [space1, space2, space3].
|
||||
func Segments(path string) []string {
|
||||
path = strings.Trim(path, types.PathSeparator)
|
||||
return strings.Split(path, types.PathSeparator)
|
||||
}
|
||||
|
||||
// IsAncesterOf returns true iff 'path' is an ancestor of 'other' or they are the same.
|
||||
// e.g. other = path(/.*)
|
||||
func IsAncesterOf(path string, other string) bool {
|
||||
path = strings.Trim(path, types.PathSeparator)
|
||||
other = strings.Trim(other, types.PathSeparator)
|
||||
|
||||
// add "/" to both to handle space1/inner and space1/in
|
||||
return strings.Contains(
|
||||
other+types.PathSeparator,
|
||||
path+types.PathSeparator,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -83,12 +83,14 @@ func (e *embedded) Detail(ctx context.Context, stage *drone.Stage) (*client.Cont
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &client.Context{
|
||||
Build: convertToDroneBuild(details.Execution),
|
||||
Repo: convertToDroneRepo(details.Repo),
|
||||
Stage: convertToDroneStage(details.Stage),
|
||||
Secrets: convertToDroneSecrets(details.Secrets),
|
||||
Config: convertToDroneFile(details.Config),
|
||||
Netrc: convertToDroneNetrc(details.Netrc),
|
||||
System: &drone.System{
|
||||
Proto: e.config.Server.HTTP.Proto,
|
||||
Host: "host.docker.internal",
|
||||
|
|
|
@ -236,3 +236,15 @@ func convertToDroneSecrets(secrets []*types.Secret) []*drone.Secret {
|
|||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func convertToDroneNetrc(netrc *Netrc) *drone.Netrc {
|
||||
if netrc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &drone.Netrc{
|
||||
Machine: netrc.Machine,
|
||||
Login: netrc.Login,
|
||||
Password: netrc.Password,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,11 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/internal/bootstrap"
|
||||
"github.com/harness/gitness/internal/jwt"
|
||||
"github.com/harness/gitness/internal/pipeline/file"
|
||||
"github.com/harness/gitness/internal/pipeline/scheduler"
|
||||
"github.com/harness/gitness/internal/sse"
|
||||
|
@ -23,6 +27,13 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// pipelineJWTLifetime specifies the max lifetime of an ephemeral pipeline jwt token.
|
||||
pipelineJWTLifetime = 72 * time.Hour
|
||||
// pipelineJWTRole specifies the role of an ephemeral pipeline jwt token.
|
||||
pipelineJWTRole = enum.MembershipRoleContributor
|
||||
)
|
||||
|
||||
var noContext = context.Background()
|
||||
|
||||
var _ ExecutionManager = (*Manager)(nil)
|
||||
|
@ -47,6 +58,14 @@ type (
|
|||
Kind string `json:"kind"`
|
||||
}
|
||||
|
||||
// Netrc contains login and initialization information used
|
||||
// by an automated login process.
|
||||
Netrc struct {
|
||||
Machine string `json:"machine"`
|
||||
Login string `json:"login"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// ExecutionContext represents the minimum amount of information
|
||||
// required by the runner to execute a build.
|
||||
ExecutionContext struct {
|
||||
|
@ -55,6 +74,7 @@ type (
|
|||
Stage *types.Stage `json:"stage"`
|
||||
Secrets []*types.Secret `json:"secrets"`
|
||||
Config *file.File `json:"config"`
|
||||
Netrc *Netrc `json:"netrc"`
|
||||
}
|
||||
|
||||
// ExecutionManager encapsulates complex build operations and provides
|
||||
|
@ -294,12 +314,44 @@ func (m *Manager) Details(ctx context.Context, stageID int64) (*ExecutionContext
|
|||
return nil, err
|
||||
}
|
||||
|
||||
netrc, err := m.createNetrc(repo)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("manager: failed to create netrc")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ExecutionContext{
|
||||
Repo: repo,
|
||||
Execution: execution,
|
||||
Stage: stage,
|
||||
Secrets: secrets,
|
||||
Config: file,
|
||||
Netrc: netrc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) createNetrc(repo *types.Repository) (*Netrc, error) {
|
||||
pipelinePrincipal := bootstrap.NewPipelineServiceSession().Principal
|
||||
jwt, err := jwt.GenerateWithMembership(
|
||||
pipelinePrincipal.ID,
|
||||
repo.ParentID,
|
||||
pipelineJWTRole,
|
||||
pipelineJWTLifetime,
|
||||
pipelinePrincipal.Salt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create jwt: %w", err)
|
||||
}
|
||||
|
||||
cloneUrl, err := url.Parse(repo.GitURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse clone url '%s': %w", cloneUrl, err)
|
||||
}
|
||||
|
||||
return &Netrc{
|
||||
Machine: cloneUrl.Hostname(),
|
||||
Login: pipelinePrincipal.UID,
|
||||
Password: jwt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -8,4 +8,9 @@ CREATE TABLE tokens (
|
|||
,token_issued_at BIGINT
|
||||
,token_created_by INTEGER
|
||||
,UNIQUE(token_principal_id, token_uid)
|
||||
|
||||
,CONSTRAINT fk_token_principal_id FOREIGN KEY (token_principal_id)
|
||||
REFERENCES principals (principal_id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE tokens ADD COLUMN token_grants BIGINT DEFAULT 0;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE tokens DROP COLUMN token_grants;
|
|
@ -8,4 +8,9 @@ CREATE TABLE tokens (
|
|||
,token_issued_at BIGINT
|
||||
,token_created_by INTEGER
|
||||
,UNIQUE(token_principal_id, token_uid COLLATE NOCASE)
|
||||
|
||||
,CONSTRAINT fk_token_principal_id FOREIGN KEY (token_principal_id)
|
||||
REFERENCES principals (principal_id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE tokens ADD COLUMN token_grants BIGINT DEFAULT 0;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE tokens DROP COLUMN token_grants;
|
|
@ -127,7 +127,6 @@ token_id
|
|||
,token_uid
|
||||
,token_principal_id
|
||||
,token_expires_at
|
||||
,token_grants
|
||||
,token_issued_at
|
||||
,token_created_by
|
||||
FROM tokens
|
||||
|
@ -168,7 +167,6 @@ INSERT INTO tokens (
|
|||
,token_uid
|
||||
,token_principal_id
|
||||
,token_expires_at
|
||||
,token_grants
|
||||
,token_issued_at
|
||||
,token_created_by
|
||||
) values (
|
||||
|
@ -176,7 +174,6 @@ INSERT INTO tokens (
|
|||
,:token_uid
|
||||
,:token_principal_id
|
||||
,:token_expires_at
|
||||
,:token_grants
|
||||
,:token_issued_at
|
||||
,:token_created_by
|
||||
) RETURNING token_id
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
issuer = "Gitness"
|
||||
)
|
||||
|
||||
// JWTClaims defines custom token claims.
|
||||
type JWTClaims struct {
|
||||
jwt.StandardClaims
|
||||
|
||||
TokenType enum.TokenType `json:"ttp,omitempty"`
|
||||
TokenID int64 `json:"tid,omitempty"`
|
||||
PrincipalID int64 `json:"pid,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateJWTForToken generates a jwt for a given token.
|
||||
func GenerateJWTForToken(token *types.Token, secret string) (string, error) {
|
||||
var expiresAt int64
|
||||
if token.ExpiresAt != nil {
|
||||
expiresAt = *token.ExpiresAt
|
||||
}
|
||||
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, JWTClaims{
|
||||
jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
// times required to be in sec not millisec
|
||||
IssuedAt: token.IssuedAt / 1000,
|
||||
ExpiresAt: expiresAt / 1000,
|
||||
},
|
||||
token.Type,
|
||||
token.ID,
|
||||
token.PrincipalID,
|
||||
})
|
||||
|
||||
res, err := jwtToken.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed to sign token")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/internal/jwt"
|
||||
"github.com/harness/gitness/internal/store"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
@ -20,10 +21,14 @@ const (
|
|||
userTokenLifeTime time.Duration = 24 * time.Hour // 1 day.
|
||||
)
|
||||
|
||||
func CreateUserSession(ctx context.Context, tokenStore store.TokenStore,
|
||||
user *types.User, uid string) (*types.Token, string, error) {
|
||||
func CreateUserSession(
|
||||
ctx context.Context,
|
||||
tokenStore store.TokenStore,
|
||||
user *types.User,
|
||||
uid string,
|
||||
) (*types.Token, string, error) {
|
||||
principal := user.ToPrincipal()
|
||||
return Create(
|
||||
return create(
|
||||
ctx,
|
||||
tokenStore,
|
||||
enum.TokenTypeSession,
|
||||
|
@ -31,14 +36,18 @@ func CreateUserSession(ctx context.Context, tokenStore store.TokenStore,
|
|||
principal,
|
||||
uid,
|
||||
ptr.Duration(userTokenLifeTime),
|
||||
enum.AccessGrantAll,
|
||||
)
|
||||
}
|
||||
|
||||
func CreatePAT(ctx context.Context, tokenStore store.TokenStore,
|
||||
createdBy *types.Principal, createdFor *types.User,
|
||||
uid string, lifetime *time.Duration, grants enum.AccessGrant) (*types.Token, string, error) {
|
||||
return Create(
|
||||
func CreatePAT(
|
||||
ctx context.Context,
|
||||
tokenStore store.TokenStore,
|
||||
createdBy *types.Principal,
|
||||
createdFor *types.User,
|
||||
uid string,
|
||||
lifetime *time.Duration,
|
||||
) (*types.Token, string, error) {
|
||||
return create(
|
||||
ctx,
|
||||
tokenStore,
|
||||
enum.TokenTypePAT,
|
||||
|
@ -46,14 +55,18 @@ func CreatePAT(ctx context.Context, tokenStore store.TokenStore,
|
|||
createdFor.ToPrincipal(),
|
||||
uid,
|
||||
lifetime,
|
||||
grants,
|
||||
)
|
||||
}
|
||||
|
||||
func CreateSAT(ctx context.Context, tokenStore store.TokenStore,
|
||||
createdBy *types.Principal, createdFor *types.ServiceAccount,
|
||||
uid string, lifetime *time.Duration, grants enum.AccessGrant) (*types.Token, string, error) {
|
||||
return Create(
|
||||
func CreateSAT(
|
||||
ctx context.Context,
|
||||
tokenStore store.TokenStore,
|
||||
createdBy *types.Principal,
|
||||
createdFor *types.ServiceAccount,
|
||||
uid string,
|
||||
lifetime *time.Duration,
|
||||
) (*types.Token, string, error) {
|
||||
return create(
|
||||
ctx,
|
||||
tokenStore,
|
||||
enum.TokenTypeSAT,
|
||||
|
@ -61,13 +74,18 @@ func CreateSAT(ctx context.Context, tokenStore store.TokenStore,
|
|||
createdFor.ToPrincipal(),
|
||||
uid,
|
||||
lifetime,
|
||||
grants,
|
||||
)
|
||||
}
|
||||
|
||||
func Create(ctx context.Context, tokenStore store.TokenStore,
|
||||
tokenType enum.TokenType, createdBy *types.Principal, createdFor *types.Principal,
|
||||
uid string, lifetime *time.Duration, grants enum.AccessGrant) (*types.Token, string, error) {
|
||||
func create(
|
||||
ctx context.Context,
|
||||
tokenStore store.TokenStore,
|
||||
tokenType enum.TokenType,
|
||||
createdBy *types.Principal,
|
||||
createdFor *types.Principal,
|
||||
uid string,
|
||||
lifetime *time.Duration,
|
||||
) (*types.Token, string, error) {
|
||||
issuedAt := time.Now()
|
||||
|
||||
var expiresAt *int64
|
||||
|
@ -82,7 +100,6 @@ func Create(ctx context.Context, tokenStore store.TokenStore,
|
|||
PrincipalID: createdFor.ID,
|
||||
IssuedAt: issuedAt.UnixMilli(),
|
||||
ExpiresAt: expiresAt,
|
||||
Grants: grants,
|
||||
CreatedBy: createdBy.ID,
|
||||
}
|
||||
|
||||
|
@ -92,7 +109,7 @@ func Create(ctx context.Context, tokenStore store.TokenStore,
|
|||
}
|
||||
|
||||
// create jwt token.
|
||||
jwtToken, err := GenerateJWTForToken(&token, createdFor.Salt)
|
||||
jwtToken, err := jwt.GenerateForToken(&token, createdFor.Salt)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to create jwt token: %w", err)
|
||||
}
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToken(t *testing.T) {
|
||||
// user := &types.User{ID: 42, Admin: true}
|
||||
// tokenStr, err := GenerateJWT(user, "TEST0E4C2F76C58916E")
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// token, err := jwt.ParseWithClaims(tokenStr, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// sub := token.Claims.(*JWTClaims).Subject
|
||||
// id, _ := strconv.ParseInt(sub, 10, 64)
|
||||
// if id != 42 {
|
||||
// t.Errorf("want subscriber id, got %v", id)
|
||||
// }
|
||||
// return []byte("TEST0E4C2F76C58916E"), nil
|
||||
// })
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// return
|
||||
// }
|
||||
// if token.Valid == false {
|
||||
// t.Errorf("invalid token")
|
||||
// return
|
||||
// }
|
||||
// if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
// t.Errorf("invalid token signing method")
|
||||
// return
|
||||
// }
|
||||
|
||||
// if expires := token.Claims.(*JWTClaims).ExpiresAt; expires > 0 {
|
||||
// if time.Now().Unix() > expires {
|
||||
// t.Errorf("token expired")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
func TestTokenExpired(t *testing.T) {
|
||||
// user := &types.User{ID: 42, Admin: true}
|
||||
// tokenStr, err := GenerateJWTWithExpiration(user, 1637549186, "TEST0E4C2F76C58916E")
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// _, err = jwt.ParseWithClaims(tokenStr, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// sub := token.Claims.(*JWTClaims).Subject
|
||||
// id, _ := strconv.ParseInt(sub, 10, 64)
|
||||
// if id != 42 {
|
||||
// t.Errorf("want subscriber id, got %v", id)
|
||||
// }
|
||||
// return []byte("TEST0E4C2F76C58916E"), nil
|
||||
// })
|
||||
// if err == nil {
|
||||
// t.Errorf("expect token expired")
|
||||
// return
|
||||
// }
|
||||
}
|
||||
|
||||
func TestTokenNotExpired(t *testing.T) {
|
||||
// user := &types.User{ID: 42, Admin: true}
|
||||
// tokenStr, err := GenerateJWTWithExpiration(user, time.Now().Add(time.Hour).Unix(), "TEST0E4C2F76C58916E")
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// token, err := jwt.ParseWithClaims(tokenStr, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// sub := token.Claims.(*JWTClaims).Subject
|
||||
// id, _ := strconv.ParseInt(sub, 10, 64)
|
||||
// if id != 42 {
|
||||
// t.Errorf("want subscriber id, got %v", id)
|
||||
// }
|
||||
// return []byte("TEST0E4C2F76C58916E"), nil
|
||||
// })
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// claims, ok := token.Claims.(*JWTClaims)
|
||||
// if !ok {
|
||||
// t.Errorf("expect token claims from token")
|
||||
// return
|
||||
// }
|
||||
|
||||
// if claims.ExpiresAt > 0 && time.Now().Unix() > claims.ExpiresAt {
|
||||
// t.Errorf("expect token not expired")
|
||||
// return
|
||||
// }
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package check
|
||||
|
||||
import (
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTokenGrantEmpty = &ValidationError{
|
||||
"The token requires at least one grant.",
|
||||
}
|
||||
)
|
||||
|
||||
// AccessGrant returns true if the access grant is valid.
|
||||
func AccessGrant(grant enum.AccessGrant, allowNone bool) error {
|
||||
if !allowNone && grant == enum.AccessGrantNone {
|
||||
return ErrTokenGrantEmpty
|
||||
}
|
||||
|
||||
// TODO: Ensure grant contains valid values?
|
||||
|
||||
return nil
|
||||
}
|
|
@ -147,7 +147,14 @@ type Config struct {
|
|||
DisplayName string `envconfig:"GITNESS_PRINCIPAL_SYSTEM_DISPLAY_NAME" default:"Gitness"`
|
||||
Email string `envconfig:"GITNESS_PRINCIPAL_SYSTEM_EMAIL" default:"system@gitness.io"`
|
||||
}
|
||||
// Pipeline defines the principal information used to create the pipeline service.
|
||||
Pipeline struct {
|
||||
UID string `envconfig:"GITNESS_PRINCIPAL_PIPELINE_UID" default:"pipeline"`
|
||||
DisplayName string `envconfig:"GITNESS_PRINCIPAL_PIPELINE_DISPLAY_NAME" default:"Gitness Pipeline"`
|
||||
Email string `envconfig:"GITNESS_PRINCIPAL_PIPELINE_EMAIL" default:"pipeline@gitness.io"`
|
||||
}
|
||||
// Admin defines the principal information used to create the admin user.
|
||||
// NOTE: The admin user is only auto-created in case a password is provided.
|
||||
Admin struct {
|
||||
UID string `envconfig:"GITNESS_PRINCIPAL_ADMIN_UID" default:"admin"`
|
||||
DisplayName string `envconfig:"GITNESS_PRINCIPAL_ADMIN_DISPLAY_NAME" default:"Administrator"`
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package enum
|
||||
|
||||
// AccessGrant represents the access grants a token or sshkey can have.
|
||||
// Keep as int64 to allow for simpler+faster lookup of grants for a given token
|
||||
// as we don't have to store an array field or need to do a join / 2nd db call.
|
||||
// Multiple grants can be combined using the bit-wise or operation.
|
||||
// ASSUMPTION: we don't need more than 63 grants!
|
||||
//
|
||||
// NOTE: A grant is always restricted by the principal permissions
|
||||
//
|
||||
// TODO: Beter name, access grant and permission might be to close in terminology?
|
||||
type AccessGrant int64
|
||||
|
||||
const (
|
||||
// no grants - useless token.
|
||||
AccessGrantNone AccessGrant = 0
|
||||
|
||||
// privacy related grants.
|
||||
AccessGrantPublic AccessGrant = 1 << 0 // 1
|
||||
AccessGrantPrivate AccessGrant = 1 << 1 // 2
|
||||
|
||||
// api related grants (spaces / repos, ...).
|
||||
AccessGrantAPICreate AccessGrant = 1 << 10 // 1024
|
||||
AccessGrantAPIView AccessGrant = 1 << 11 // 2048
|
||||
AccessGrantAPIEdit AccessGrant = 1 << 12 // 4096
|
||||
AccessGrantAPIDelete AccessGrant = 1 << 13 // 8192
|
||||
|
||||
// code related grants.
|
||||
AccessGrantCodeRead AccessGrant = 1 << 20 // 1048576
|
||||
AccessGrantCodeWrite AccessGrant = 1 << 21 // 2097152
|
||||
|
||||
// grants everything - for user sessions.
|
||||
AccessGrantAll AccessGrant = 1<<63 - 1
|
||||
)
|
||||
|
||||
// DoesGrantContain checks whether the grants contain all grants in the provided grant.
|
||||
func (g AccessGrant) Contains(grants AccessGrant) bool {
|
||||
return g&grants == grants
|
||||
}
|
||||
|
||||
// CombineGrants combines all grants into a single grant.
|
||||
// Note: duplicates are ignored.
|
||||
func CombineGrants(grants ...AccessGrant) AccessGrant {
|
||||
res := AccessGrantNone
|
||||
|
||||
for _, grant := range grants {
|
||||
res |= grant
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
|
@ -18,9 +18,8 @@ type Token struct {
|
|||
// ExpiresAt is an optional unix time that if specified restricts the validity of a token.
|
||||
ExpiresAt *int64 `db:"token_expires_at" json:"expires_at,omitempty"`
|
||||
// IssuedAt is the unix time at which the token was issued.
|
||||
IssuedAt int64 `db:"token_issued_at" json:"issued_at"`
|
||||
Grants enum.AccessGrant `db:"token_grants" json:"grants"`
|
||||
CreatedBy int64 `db:"token_created_by" json:"created_by"`
|
||||
IssuedAt int64 `db:"token_issued_at" json:"issued_at"`
|
||||
CreatedBy int64 `db:"token_created_by" json:"created_by"`
|
||||
}
|
||||
|
||||
// TokenResponse is returned as part of token creation for PAT / SAT / User Session.
|
||||
|
|
|
@ -5,8 +5,6 @@ import { Get, GetProps, useGet, UseGetProps, Mutate, MutateProps, useMutate, Use
|
|||
|
||||
import { getConfig } from '../config'
|
||||
export const SPEC_VERSION = '0.0.0'
|
||||
export type EnumAccessGrant = number
|
||||
|
||||
export type EnumCIStatus = string
|
||||
|
||||
export type EnumCheckPayloadKind = '' | 'markdown' | 'pipeline' | 'raw'
|
||||
|
@ -270,7 +268,6 @@ export interface OpenapiCreateTemplateRequest {
|
|||
}
|
||||
|
||||
export interface OpenapiCreateTokenRequest {
|
||||
grants?: EnumAccessGrant
|
||||
lifetime?: TimeDuration
|
||||
uid?: string
|
||||
}
|
||||
|
@ -872,7 +869,6 @@ export interface TypesTemplate {
|
|||
export interface TypesToken {
|
||||
created_by?: number
|
||||
expires_at?: number | null
|
||||
grants?: EnumAccessGrant
|
||||
issued_at?: number
|
||||
principal_id?: number
|
||||
type?: EnumTokenType
|
||||
|
|
|
@ -6277,8 +6277,6 @@ paths:
|
|||
- user
|
||||
components:
|
||||
schemas:
|
||||
EnumAccessGrant:
|
||||
type: integer
|
||||
EnumCIStatus:
|
||||
type: string
|
||||
EnumCheckPayloadKind:
|
||||
|
@ -6745,8 +6743,6 @@ components:
|
|||
type: object
|
||||
OpenapiCreateTokenRequest:
|
||||
properties:
|
||||
grants:
|
||||
$ref: '#/components/schemas/EnumAccessGrant'
|
||||
lifetime:
|
||||
$ref: '#/components/schemas/TimeDuration'
|
||||
uid:
|
||||
|
@ -7805,8 +7801,6 @@ components:
|
|||
expires_at:
|
||||
nullable: true
|
||||
type: integer
|
||||
grants:
|
||||
$ref: '#/components/schemas/EnumAccessGrant'
|
||||
issued_at:
|
||||
type: integer
|
||||
principal_id:
|
||||
|
|
Loading…
Reference in New Issue