mirror of
https://github.com/harness/drone.git
synced 2025-05-17 09:30:00 +08:00
Merge remote-tracking branch 'origin' into abhinav/CODE-852
This commit is contained in:
commit
6e99fc903d
@ -13,7 +13,6 @@ import (
|
|||||||
|
|
||||||
"github.com/harness/gitness/cli/provide"
|
"github.com/harness/gitness/cli/provide"
|
||||||
"github.com/harness/gitness/internal/api/controller/user"
|
"github.com/harness/gitness/internal/api/controller/user"
|
||||||
"github.com/harness/gitness/types/enum"
|
|
||||||
|
|
||||||
"github.com/drone/funcmap"
|
"github.com/drone/funcmap"
|
||||||
"github.com/gotidy/ptr"
|
"github.com/gotidy/ptr"
|
||||||
@ -47,7 +46,6 @@ func (c *createPATCommand) run(*kingpin.ParseContext) error {
|
|||||||
in := user.CreateTokenInput{
|
in := user.CreateTokenInput{
|
||||||
UID: c.uid,
|
UID: c.uid,
|
||||||
Lifetime: lifeTime,
|
Lifetime: lifeTime,
|
||||||
Grants: enum.AccessGrantAll,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenResp, err := provide.Client().UserCreatePAT(ctx, in)
|
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)
|
principalInfoCache := cache.ProvidePrincipalInfoCache(principalInfoView)
|
||||||
membershipStore := database.ProvideMembershipStore(db, principalInfoCache)
|
membershipStore := database.ProvideMembershipStore(db, principalInfoCache)
|
||||||
permissionCache := authz.ProvidePermissionCache(spaceStore, membershipStore)
|
permissionCache := authz.ProvidePermissionCache(spaceStore, membershipStore)
|
||||||
authorizer := authz.ProvideAuthorizer(permissionCache)
|
authorizer := authz.ProvideAuthorizer(permissionCache, spaceStore)
|
||||||
principalUIDTransformation := store.ProvidePrincipalUIDTransformation()
|
principalUIDTransformation := store.ProvidePrincipalUIDTransformation()
|
||||||
principalStore := database.ProvidePrincipalStore(db, principalUIDTransformation)
|
principalStore := database.ProvidePrincipalStore(db, principalUIDTransformation)
|
||||||
tokenStore := database.ProvideTokenStore(db)
|
tokenStore := database.ProvideTokenStore(db)
|
||||||
|
@ -17,14 +17,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateTokenInput struct {
|
type CreateTokenInput struct {
|
||||||
UID string `json:"uid"`
|
UID string `json:"uid"`
|
||||||
Lifetime *time.Duration `json:"lifetime"`
|
Lifetime *time.Duration `json:"lifetime"`
|
||||||
Grants enum.AccessGrant `json:"grants"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateToken creates a new service account access token.
|
// CreateToken creates a new service account access token.
|
||||||
func (c *Controller) CreateToken(ctx context.Context, session *auth.Session,
|
func (c *Controller) CreateToken(
|
||||||
saUID string, in *CreateTokenInput) (*types.TokenResponse, error) {
|
ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
saUID string,
|
||||||
|
in *CreateTokenInput,
|
||||||
|
) (*types.TokenResponse, error) {
|
||||||
sa, err := findServiceAccountFromUID(ctx, c.principalStore, saUID)
|
sa, err := findServiceAccountFromUID(ctx, c.principalStore, saUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err = check.TokenLifetime(in.Lifetime, true); err != nil {
|
||||||
return nil, err
|
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)
|
// Ensure principal has required permissions on parent (ensures that parent exists)
|
||||||
if err = apiauth.CheckServiceAccount(ctx, c.authorizer, session, c.spaceStore, c.repoStore,
|
if err = apiauth.CheckServiceAccount(ctx, c.authorizer, session, c.spaceStore, c.repoStore,
|
||||||
sa.ParentType, sa.ParentID, sa.UID, enum.PermissionServiceAccountEdit); err != nil {
|
sa.ParentType, sa.ParentID, sa.UID, enum.PermissionServiceAccountEdit); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
token, jwtToken, err := token.CreateSAT(ctx, c.tokenStore, &session.Principal,
|
token, jwtToken, err := token.CreateSAT(
|
||||||
sa, in.UID, in.Lifetime, in.Grants)
|
ctx,
|
||||||
|
c.tokenStore,
|
||||||
|
&session.Principal,
|
||||||
|
sa,
|
||||||
|
in.UID,
|
||||||
|
in.Lifetime,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -17,16 +17,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateTokenInput struct {
|
type CreateTokenInput struct {
|
||||||
UID string `json:"uid"`
|
UID string `json:"uid"`
|
||||||
Lifetime *time.Duration `json:"lifetime"`
|
Lifetime *time.Duration `json:"lifetime"`
|
||||||
Grants enum.AccessGrant `json:"grants"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CreateToken creates a new user access token.
|
* CreateToken creates a new user access token.
|
||||||
*/
|
*/
|
||||||
func (c *Controller) CreateAccessToken(ctx context.Context, session *auth.Session,
|
func (c *Controller) CreateAccessToken(
|
||||||
userUID string, in *CreateTokenInput) (*types.TokenResponse, error) {
|
ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
userUID string,
|
||||||
|
in *CreateTokenInput,
|
||||||
|
) (*types.TokenResponse, error) {
|
||||||
user, err := findUserFromUID(ctx, c.principalStore, userUID)
|
user, err := findUserFromUID(ctx, c.principalStore, userUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err = check.TokenLifetime(in.Lifetime, true); err != nil {
|
||||||
return nil, err
|
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,
|
token, jwtToken, err := token.CreatePAT(
|
||||||
user, in.UID, in.Lifetime, in.Grants)
|
ctx,
|
||||||
|
c.tokenStore,
|
||||||
|
&session.Principal,
|
||||||
|
user,
|
||||||
|
in.UID,
|
||||||
|
in.Lifetime,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ func (c *Controller) Logout(ctx context.Context, session *auth.Session) error {
|
|||||||
tokenID = t.TokenID
|
tokenID = t.TokenID
|
||||||
tokenType = t.TokenType
|
tokenType = t.TokenType
|
||||||
default:
|
default:
|
||||||
return errors.New("session metadata is of unknown type")
|
return errors.New("provided jwt doesn't support logout")
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenType != enum.TokenTypeSession {
|
if tokenType != enum.TokenTypeSession {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package authn
|
package authn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -12,34 +13,31 @@ import (
|
|||||||
|
|
||||||
"github.com/harness/gitness/internal/api/request"
|
"github.com/harness/gitness/internal/api/request"
|
||||||
"github.com/harness/gitness/internal/auth"
|
"github.com/harness/gitness/internal/auth"
|
||||||
|
"github.com/harness/gitness/internal/jwt"
|
||||||
"github.com/harness/gitness/internal/store"
|
"github.com/harness/gitness/internal/store"
|
||||||
"github.com/harness/gitness/internal/token"
|
|
||||||
"github.com/harness/gitness/types"
|
"github.com/harness/gitness/types"
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
gojwt "github.com/dgrijalva/jwt-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Authenticator = (*TokenAuthenticator)(nil)
|
var _ Authenticator = (*JWTAuthenticator)(nil)
|
||||||
|
|
||||||
/*
|
// JWTAuthenticator uses the provided JWT to authenticate the caller.
|
||||||
* Authenticates a user by checking for an access token in the
|
type JWTAuthenticator struct {
|
||||||
* "Authorization" header or the "access_token" form value.
|
|
||||||
*/
|
|
||||||
type TokenAuthenticator struct {
|
|
||||||
principalStore store.PrincipalStore
|
principalStore store.PrincipalStore
|
||||||
tokenStore store.TokenStore
|
tokenStore store.TokenStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTokenAuthenticator(
|
func NewTokenAuthenticator(
|
||||||
principalStore store.PrincipalStore,
|
principalStore store.PrincipalStore,
|
||||||
tokenStore store.TokenStore) *TokenAuthenticator {
|
tokenStore store.TokenStore) *JWTAuthenticator {
|
||||||
return &TokenAuthenticator{
|
return &JWTAuthenticator{
|
||||||
principalStore: principalStore,
|
principalStore: principalStore,
|
||||||
tokenStore: tokenStore,
|
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()
|
ctx := r.Context()
|
||||||
str := extractToken(r)
|
str := extractToken(r)
|
||||||
|
|
||||||
@ -49,8 +47,8 @@ func (a *TokenAuthenticator) Authenticate(r *http.Request, sourceRouter SourceRo
|
|||||||
|
|
||||||
var principal *types.Principal
|
var principal *types.Principal
|
||||||
var err error
|
var err error
|
||||||
claims := &token.JWTClaims{}
|
claims := &jwt.Claims{}
|
||||||
parsed, err := jwt.ParseWithClaims(str, claims, func(token_ *jwt.Token) (interface{}, error) {
|
parsed, err := gojwt.ParseWithClaims(str, claims, func(token_ *gojwt.Token) (interface{}, error) {
|
||||||
principal, err = a.principalStore.Find(ctx, claims.PrincipalID)
|
principal, err = a.principalStore.Find(ctx, claims.PrincipalID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get principal for token: %w", err)
|
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")
|
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")
|
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
|
// ensure tkn exists
|
||||||
tkn, err := a.tokenStore.Find(ctx, claims.TokenID)
|
tkn, err := a.tokenStore.Find(ctx, tknClaims.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to find token in db: %w", err)
|
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)
|
principal.ID, tkn.PrincipalID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &auth.Session{
|
return &auth.TokenMetadata{
|
||||||
Principal: *principal,
|
TokenType: tkn.Type,
|
||||||
Metadata: &auth.TokenMetadata{
|
TokenID: tkn.ID,
|
||||||
TokenType: tkn.Type,
|
}, nil
|
||||||
TokenID: tkn.ID,
|
}
|
||||||
Grants: tkn.Grants,
|
|
||||||
},
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -6,9 +6,11 @@ package authz
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/harness/gitness/internal/auth"
|
"github.com/harness/gitness/internal/auth"
|
||||||
"github.com/harness/gitness/internal/paths"
|
"github.com/harness/gitness/internal/paths"
|
||||||
|
"github.com/harness/gitness/internal/store"
|
||||||
"github.com/harness/gitness/types"
|
"github.com/harness/gitness/types"
|
||||||
"github.com/harness/gitness/types/enum"
|
"github.com/harness/gitness/types/enum"
|
||||||
|
|
||||||
@ -19,13 +21,16 @@ var _ Authorizer = (*MembershipAuthorizer)(nil)
|
|||||||
|
|
||||||
type MembershipAuthorizer struct {
|
type MembershipAuthorizer struct {
|
||||||
permissionCache PermissionCache
|
permissionCache PermissionCache
|
||||||
|
spaceStore store.SpaceStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMembershipAuthorizer(
|
func NewMembershipAuthorizer(
|
||||||
permissionCache PermissionCache,
|
permissionCache PermissionCache,
|
||||||
|
spaceStore store.SpaceStore,
|
||||||
) *MembershipAuthorizer {
|
) *MembershipAuthorizer {
|
||||||
return &MembershipAuthorizer{
|
return &MembershipAuthorizer{
|
||||||
permissionCache: permissionCache,
|
permissionCache: permissionCache,
|
||||||
|
spaceStore: spaceStore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,23 +56,23 @@ func (a *MembershipAuthorizer) Check(
|
|||||||
return true, nil // system admin can call any API
|
return true, nil // system admin can call any API
|
||||||
}
|
}
|
||||||
|
|
||||||
var spaceRef string
|
var spacePath string
|
||||||
|
|
||||||
switch resource.Type {
|
switch resource.Type {
|
||||||
case enum.ResourceTypeSpace:
|
case enum.ResourceTypeSpace:
|
||||||
spaceRef = paths.Concatinate(scope.SpacePath, resource.Name)
|
spacePath = paths.Concatinate(scope.SpacePath, resource.Name)
|
||||||
|
|
||||||
case enum.ResourceTypeRepo:
|
case enum.ResourceTypeRepo:
|
||||||
spaceRef = scope.SpacePath
|
spacePath = scope.SpacePath
|
||||||
|
|
||||||
case enum.ResourceTypeServiceAccount:
|
case enum.ResourceTypeServiceAccount:
|
||||||
spaceRef = scope.SpacePath
|
spacePath = scope.SpacePath
|
||||||
|
|
||||||
case enum.ResourceTypePipeline:
|
case enum.ResourceTypePipeline:
|
||||||
spaceRef = scope.SpacePath
|
spacePath = scope.SpacePath
|
||||||
|
|
||||||
case enum.ResourceTypeSecret:
|
case enum.ResourceTypeSecret:
|
||||||
spaceRef = scope.SpacePath
|
spacePath = scope.SpacePath
|
||||||
|
|
||||||
case enum.ResourceTypeUser:
|
case enum.ResourceTypeUser:
|
||||||
// a user is allowed to view / edit themselves
|
// a user is allowed to view / edit themselves
|
||||||
@ -87,12 +92,23 @@ func (a *MembershipAuthorizer) Check(
|
|||||||
return false, nil
|
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{
|
return a.permissionCache.Get(ctx, PermissionCacheKey{
|
||||||
PrincipalID: session.Principal.ID,
|
PrincipalID: session.Principal.ID,
|
||||||
SpaceRef: spaceRef,
|
SpaceRef: spacePath,
|
||||||
Permission: permission,
|
Permission: permission,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *MembershipAuthorizer) CheckAll(ctx context.Context, session *auth.Session,
|
func (a *MembershipAuthorizer) CheckAll(ctx context.Context, session *auth.Session,
|
||||||
permissionChecks ...types.PermissionCheck) (bool, error) {
|
permissionChecks ...types.PermissionCheck) (bool, error) {
|
||||||
for _, p := range permissionChecks {
|
for _, p := range permissionChecks {
|
||||||
@ -103,3 +119,35 @@ func (a *MembershipAuthorizer) CheckAll(ctx context.Context, session *auth.Sessi
|
|||||||
|
|
||||||
return true, nil
|
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 the membership is defined in the current space, check if the user has the required permission.
|
||||||
if membership != nil {
|
if membership != nil &&
|
||||||
_, hasRole := slices.BinarySearch(membership.Role.Permissions(), key.Permission)
|
roleHasPermission(membership.Role, key.Permission) {
|
||||||
if hasRole {
|
return true, nil
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If membership with the requested permission has not been found in the current space,
|
// 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
|
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,
|
ProvidePermissionCache,
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideAuthorizer(pCache PermissionCache) Authorizer {
|
func ProvideAuthorizer(pCache PermissionCache, spaceStore store.SpaceStore) Authorizer {
|
||||||
return NewMembershipAuthorizer(pCache)
|
return NewMembershipAuthorizer(pCache, spaceStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvidePermissionCache(
|
func ProvidePermissionCache(
|
||||||
|
@ -17,23 +17,22 @@ func (m *EmptyMetadata) ImpactsAuthorization() bool {
|
|||||||
return false
|
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.
|
// TokenMetadata contains information about the token that was used during auth.
|
||||||
type TokenMetadata struct {
|
type TokenMetadata struct {
|
||||||
TokenType enum.TokenType
|
TokenType enum.TokenType
|
||||||
TokenID int64
|
TokenID int64
|
||||||
Grants enum.AccessGrant // retrieved from token during verification
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *TokenMetadata) ImpactsAuthorization() bool {
|
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.
|
// Bootstrap is an abstraction of a function that bootstraps a system.
|
||||||
type Bootstrap func(context.Context) error
|
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 {
|
serviceCtrl *service.Controller) func(context.Context) error {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if err := SystemService(ctx, config, serviceCtrl); err != nil {
|
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 {
|
if err := AdminUser(ctx, config, userCtrl); err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to setup admin user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -70,7 +85,11 @@ func AdminUser(ctx context.Context, config *types.Config, userCtrl *user.Control
|
|||||||
return nil
|
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{
|
in := &user.CreateInput{
|
||||||
UID: config.Principal.Admin.UID,
|
UID: config.Principal.Admin.UID,
|
||||||
DisplayName: config.Principal.Admin.DisplayName,
|
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
|
// SystemService sets up the gitness service principal that is used for
|
||||||
// resources that are automatically created by the system.
|
// 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)
|
svc, err := serviceCtrl.FindNoAuth(ctx, config.Principal.System.UID)
|
||||||
if errors.Is(err, store.ErrResourceNotFound) {
|
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 {
|
if err != nil {
|
||||||
@ -116,25 +146,65 @@ func SystemService(ctx context.Context, config *types.Config, serviceCtrl *servi
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSystemService(ctx context.Context, config *types.Config,
|
// PipelineService sets up the pipeline service principal that is used during
|
||||||
serviceCtrl *service.Controller) (*types.Service, error) {
|
// pipeline executions for calling gitness APIs.
|
||||||
in := &service.CreateInput{
|
func PipelineService(
|
||||||
UID: config.Principal.System.UID,
|
ctx context.Context,
|
||||||
Email: config.Principal.System.Email,
|
config *types.Config,
|
||||||
DisplayName: config.Principal.System.DisplayName,
|
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) {
|
if createErr == nil || !errors.Is(createErr, store.ErrDuplicate) {
|
||||||
return svc, createErr
|
return svc, createErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// service might've been created by another instance in which case we should find it now.
|
// service might've been created by another instance in which case we should find it now.
|
||||||
var findErr error
|
var findErr error
|
||||||
svc, findErr = serviceCtrl.FindNoAuth(ctx, config.Principal.System.UID)
|
svc, findErr = serviceCtrl.FindNoAuth(ctx, uid)
|
||||||
if findErr != nil {
|
if findErr != nil {
|
||||||
return nil, fmt.Errorf("failed to find service with uid '%s' (%s) after duplicate error: %w",
|
return nil, fmt.Errorf(
|
||||||
config.Principal.System.UID, findErr, createErr)
|
"failed to find service with uid '%s' (%s) after duplicate error: %w",
|
||||||
|
uid,
|
||||||
|
findErr,
|
||||||
|
createErr,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return svc, nil
|
return svc, nil
|
||||||
|
97
internal/jwt/jwt.go
Normal file
97
internal/jwt/jwt.go
Normal file
@ -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
|
// DisectLeaf splits a path into its parent path and the leaf name
|
||||||
// e.g. space1/space2/space3 -> (space1/space2, space3, nil).
|
// e.g. space1/space2/space3 -> (space1/space2, space3, nil).
|
||||||
func DisectLeaf(path string) (string, string, error) {
|
func DisectLeaf(path string) (string, string, error) {
|
||||||
|
path = strings.Trim(path, types.PathSeparator)
|
||||||
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return "", "", ErrPathEmpty
|
return "", "", ErrPathEmpty
|
||||||
}
|
}
|
||||||
@ -33,6 +35,8 @@ func DisectLeaf(path string) (string, string, error) {
|
|||||||
// DisectRoot splits a path into its root space and sub-path
|
// DisectRoot splits a path into its root space and sub-path
|
||||||
// e.g. space1/space2/space3 -> (space1, space2/space3, nil).
|
// e.g. space1/space2/space3 -> (space1, space2/space3, nil).
|
||||||
func DisectRoot(path string) (string, string, error) {
|
func DisectRoot(path string) (string, string, error) {
|
||||||
|
path = strings.Trim(path, types.PathSeparator)
|
||||||
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return "", "", ErrPathEmpty
|
return "", "", ErrPathEmpty
|
||||||
}
|
}
|
||||||
@ -67,5 +71,19 @@ func Concatinate(path1 string, path2 string) string {
|
|||||||
// Segments returns all segments of the path
|
// Segments returns all segments of the path
|
||||||
// e.g. /space1/space2/space3 -> [space1, space2, space3].
|
// e.g. /space1/space2/space3 -> [space1, space2, space3].
|
||||||
func Segments(path string) []string {
|
func Segments(path string) []string {
|
||||||
|
path = strings.Trim(path, types.PathSeparator)
|
||||||
return strings.Split(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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &client.Context{
|
return &client.Context{
|
||||||
Build: convertToDroneBuild(details.Execution),
|
Build: convertToDroneBuild(details.Execution),
|
||||||
Repo: convertToDroneRepo(details.Repo),
|
Repo: convertToDroneRepo(details.Repo),
|
||||||
Stage: convertToDroneStage(details.Stage),
|
Stage: convertToDroneStage(details.Stage),
|
||||||
Secrets: convertToDroneSecrets(details.Secrets),
|
Secrets: convertToDroneSecrets(details.Secrets),
|
||||||
Config: convertToDroneFile(details.Config),
|
Config: convertToDroneFile(details.Config),
|
||||||
|
Netrc: convertToDroneNetrc(details.Netrc),
|
||||||
System: &drone.System{
|
System: &drone.System{
|
||||||
Proto: e.config.Server.HTTP.Proto,
|
Proto: e.config.Server.HTTP.Proto,
|
||||||
Host: "host.docker.internal",
|
Host: "host.docker.internal",
|
||||||
|
@ -236,3 +236,15 @@ func convertToDroneSecrets(secrets []*types.Secret) []*drone.Secret {
|
|||||||
}
|
}
|
||||||
return ret
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"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/file"
|
||||||
"github.com/harness/gitness/internal/pipeline/scheduler"
|
"github.com/harness/gitness/internal/pipeline/scheduler"
|
||||||
"github.com/harness/gitness/internal/sse"
|
"github.com/harness/gitness/internal/sse"
|
||||||
@ -23,6 +27,13 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"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 noContext = context.Background()
|
||||||
|
|
||||||
var _ ExecutionManager = (*Manager)(nil)
|
var _ ExecutionManager = (*Manager)(nil)
|
||||||
@ -47,6 +58,14 @@ type (
|
|||||||
Kind string `json:"kind"`
|
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
|
// ExecutionContext represents the minimum amount of information
|
||||||
// required by the runner to execute a build.
|
// required by the runner to execute a build.
|
||||||
ExecutionContext struct {
|
ExecutionContext struct {
|
||||||
@ -55,6 +74,7 @@ type (
|
|||||||
Stage *types.Stage `json:"stage"`
|
Stage *types.Stage `json:"stage"`
|
||||||
Secrets []*types.Secret `json:"secrets"`
|
Secrets []*types.Secret `json:"secrets"`
|
||||||
Config *file.File `json:"config"`
|
Config *file.File `json:"config"`
|
||||||
|
Netrc *Netrc `json:"netrc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecutionManager encapsulates complex build operations and provides
|
// ExecutionManager encapsulates complex build operations and provides
|
||||||
@ -294,12 +314,44 @@ func (m *Manager) Details(ctx context.Context, stageID int64) (*ExecutionContext
|
|||||||
return nil, err
|
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{
|
return &ExecutionContext{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Execution: execution,
|
Execution: execution,
|
||||||
Stage: stage,
|
Stage: stage,
|
||||||
Secrets: secrets,
|
Secrets: secrets,
|
||||||
Config: file,
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,4 +8,9 @@ CREATE TABLE tokens (
|
|||||||
,token_issued_at BIGINT
|
,token_issued_at BIGINT
|
||||||
,token_created_by INTEGER
|
,token_created_by INTEGER
|
||||||
,UNIQUE(token_principal_id, token_uid)
|
,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_issued_at BIGINT
|
||||||
,token_created_by INTEGER
|
,token_created_by INTEGER
|
||||||
,UNIQUE(token_principal_id, token_uid COLLATE NOCASE)
|
,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_uid
|
||||||
,token_principal_id
|
,token_principal_id
|
||||||
,token_expires_at
|
,token_expires_at
|
||||||
,token_grants
|
|
||||||
,token_issued_at
|
,token_issued_at
|
||||||
,token_created_by
|
,token_created_by
|
||||||
FROM tokens
|
FROM tokens
|
||||||
@ -168,7 +167,6 @@ INSERT INTO tokens (
|
|||||||
,token_uid
|
,token_uid
|
||||||
,token_principal_id
|
,token_principal_id
|
||||||
,token_expires_at
|
,token_expires_at
|
||||||
,token_grants
|
|
||||||
,token_issued_at
|
,token_issued_at
|
||||||
,token_created_by
|
,token_created_by
|
||||||
) values (
|
) values (
|
||||||
@ -176,7 +174,6 @@ INSERT INTO tokens (
|
|||||||
,:token_uid
|
,:token_uid
|
||||||
,:token_principal_id
|
,:token_principal_id
|
||||||
,:token_expires_at
|
,:token_expires_at
|
||||||
,:token_grants
|
|
||||||
,:token_issued_at
|
,:token_issued_at
|
||||||
,:token_created_by
|
,:token_created_by
|
||||||
) RETURNING token_id
|
) RETURNING token_id
|
||||||
|
@ -187,6 +187,7 @@ func (s *triggerStore) Update(ctx context.Context, t *types.Trigger) error {
|
|||||||
SET
|
SET
|
||||||
trigger_uid = :trigger_uid
|
trigger_uid = :trigger_uid
|
||||||
,trigger_description = :trigger_description
|
,trigger_description = :trigger_description
|
||||||
|
,trigger_disabled = :trigger_disabled
|
||||||
,trigger_updated = :trigger_updated
|
,trigger_updated = :trigger_updated
|
||||||
,trigger_actions = :trigger_actions
|
,trigger_actions = :trigger_actions
|
||||||
,trigger_version = :trigger_version
|
,trigger_version = :trigger_version
|
||||||
|
@ -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"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/internal/jwt"
|
||||||
"github.com/harness/gitness/internal/store"
|
"github.com/harness/gitness/internal/store"
|
||||||
"github.com/harness/gitness/types"
|
"github.com/harness/gitness/types"
|
||||||
"github.com/harness/gitness/types/enum"
|
"github.com/harness/gitness/types/enum"
|
||||||
@ -20,10 +21,14 @@ const (
|
|||||||
userTokenLifeTime time.Duration = 24 * time.Hour // 1 day.
|
userTokenLifeTime time.Duration = 24 * time.Hour // 1 day.
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateUserSession(ctx context.Context, tokenStore store.TokenStore,
|
func CreateUserSession(
|
||||||
user *types.User, uid string) (*types.Token, string, error) {
|
ctx context.Context,
|
||||||
|
tokenStore store.TokenStore,
|
||||||
|
user *types.User,
|
||||||
|
uid string,
|
||||||
|
) (*types.Token, string, error) {
|
||||||
principal := user.ToPrincipal()
|
principal := user.ToPrincipal()
|
||||||
return Create(
|
return create(
|
||||||
ctx,
|
ctx,
|
||||||
tokenStore,
|
tokenStore,
|
||||||
enum.TokenTypeSession,
|
enum.TokenTypeSession,
|
||||||
@ -31,14 +36,18 @@ func CreateUserSession(ctx context.Context, tokenStore store.TokenStore,
|
|||||||
principal,
|
principal,
|
||||||
uid,
|
uid,
|
||||||
ptr.Duration(userTokenLifeTime),
|
ptr.Duration(userTokenLifeTime),
|
||||||
enum.AccessGrantAll,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePAT(ctx context.Context, tokenStore store.TokenStore,
|
func CreatePAT(
|
||||||
createdBy *types.Principal, createdFor *types.User,
|
ctx context.Context,
|
||||||
uid string, lifetime *time.Duration, grants enum.AccessGrant) (*types.Token, string, error) {
|
tokenStore store.TokenStore,
|
||||||
return Create(
|
createdBy *types.Principal,
|
||||||
|
createdFor *types.User,
|
||||||
|
uid string,
|
||||||
|
lifetime *time.Duration,
|
||||||
|
) (*types.Token, string, error) {
|
||||||
|
return create(
|
||||||
ctx,
|
ctx,
|
||||||
tokenStore,
|
tokenStore,
|
||||||
enum.TokenTypePAT,
|
enum.TokenTypePAT,
|
||||||
@ -46,14 +55,18 @@ func CreatePAT(ctx context.Context, tokenStore store.TokenStore,
|
|||||||
createdFor.ToPrincipal(),
|
createdFor.ToPrincipal(),
|
||||||
uid,
|
uid,
|
||||||
lifetime,
|
lifetime,
|
||||||
grants,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateSAT(ctx context.Context, tokenStore store.TokenStore,
|
func CreateSAT(
|
||||||
createdBy *types.Principal, createdFor *types.ServiceAccount,
|
ctx context.Context,
|
||||||
uid string, lifetime *time.Duration, grants enum.AccessGrant) (*types.Token, string, error) {
|
tokenStore store.TokenStore,
|
||||||
return Create(
|
createdBy *types.Principal,
|
||||||
|
createdFor *types.ServiceAccount,
|
||||||
|
uid string,
|
||||||
|
lifetime *time.Duration,
|
||||||
|
) (*types.Token, string, error) {
|
||||||
|
return create(
|
||||||
ctx,
|
ctx,
|
||||||
tokenStore,
|
tokenStore,
|
||||||
enum.TokenTypeSAT,
|
enum.TokenTypeSAT,
|
||||||
@ -61,13 +74,18 @@ func CreateSAT(ctx context.Context, tokenStore store.TokenStore,
|
|||||||
createdFor.ToPrincipal(),
|
createdFor.ToPrincipal(),
|
||||||
uid,
|
uid,
|
||||||
lifetime,
|
lifetime,
|
||||||
grants,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Create(ctx context.Context, tokenStore store.TokenStore,
|
func create(
|
||||||
tokenType enum.TokenType, createdBy *types.Principal, createdFor *types.Principal,
|
ctx context.Context,
|
||||||
uid string, lifetime *time.Duration, grants enum.AccessGrant) (*types.Token, string, error) {
|
tokenStore store.TokenStore,
|
||||||
|
tokenType enum.TokenType,
|
||||||
|
createdBy *types.Principal,
|
||||||
|
createdFor *types.Principal,
|
||||||
|
uid string,
|
||||||
|
lifetime *time.Duration,
|
||||||
|
) (*types.Token, string, error) {
|
||||||
issuedAt := time.Now()
|
issuedAt := time.Now()
|
||||||
|
|
||||||
var expiresAt *int64
|
var expiresAt *int64
|
||||||
@ -82,7 +100,6 @@ func Create(ctx context.Context, tokenStore store.TokenStore,
|
|||||||
PrincipalID: createdFor.ID,
|
PrincipalID: createdFor.ID,
|
||||||
IssuedAt: issuedAt.UnixMilli(),
|
IssuedAt: issuedAt.UnixMilli(),
|
||||||
ExpiresAt: expiresAt,
|
ExpiresAt: expiresAt,
|
||||||
Grants: grants,
|
|
||||||
CreatedBy: createdBy.ID,
|
CreatedBy: createdBy.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +109,7 @@ func Create(ctx context.Context, tokenStore store.TokenStore,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create jwt token.
|
// create jwt token.
|
||||||
jwtToken, err := GenerateJWTForToken(&token, createdFor.Salt)
|
jwtToken, err := jwt.GenerateForToken(&token, createdFor.Salt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to create jwt token: %w", err)
|
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"`
|
DisplayName string `envconfig:"GITNESS_PRINCIPAL_SYSTEM_DISPLAY_NAME" default:"Gitness"`
|
||||||
Email string `envconfig:"GITNESS_PRINCIPAL_SYSTEM_EMAIL" default:"system@gitness.io"`
|
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.
|
// 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 {
|
Admin struct {
|
||||||
UID string `envconfig:"GITNESS_PRINCIPAL_ADMIN_UID" default:"admin"`
|
UID string `envconfig:"GITNESS_PRINCIPAL_ADMIN_UID" default:"admin"`
|
||||||
DisplayName string `envconfig:"GITNESS_PRINCIPAL_ADMIN_DISPLAY_NAME" default:"Administrator"`
|
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 is an optional unix time that if specified restricts the validity of a token.
|
||||||
ExpiresAt *int64 `db:"token_expires_at" json:"expires_at,omitempty"`
|
ExpiresAt *int64 `db:"token_expires_at" json:"expires_at,omitempty"`
|
||||||
// IssuedAt is the unix time at which the token was issued.
|
// IssuedAt is the unix time at which the token was issued.
|
||||||
IssuedAt int64 `db:"token_issued_at" json:"issued_at"`
|
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"`
|
||||||
CreatedBy int64 `db:"token_created_by" json:"created_by"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenResponse is returned as part of token creation for PAT / SAT / User Session.
|
// TokenResponse is returned as part of token creation for PAT / SAT / User Session.
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
.actionsContainer {
|
||||||
|
background-color: var(--grey-50) !important;
|
||||||
|
width: 100% !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
border: 1px solid rgba(217, 218, 229, 0.5) !important;
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
gap: 10px !important;
|
||||||
|
}
|
3
web/src/components/NewTriggerModalButton/NewTriggerModalButton.module.scss.d.ts
vendored
Normal file
3
web/src/components/NewTriggerModalButton/NewTriggerModalButton.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// This is an auto-generated file
|
||||||
|
export declare const actionsContainer: string
|
@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
useToaster,
|
||||||
|
type ButtonProps,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
Layout,
|
||||||
|
Container,
|
||||||
|
Formik,
|
||||||
|
FormikForm,
|
||||||
|
FormInput,
|
||||||
|
FlexExpander,
|
||||||
|
Text,
|
||||||
|
Checkbox
|
||||||
|
} from '@harnessio/uicore'
|
||||||
|
import { Icon } from '@harnessio/icons'
|
||||||
|
import { FontVariation, Intent } from '@harnessio/design-system'
|
||||||
|
import React from 'react'
|
||||||
|
import { useMutate } from 'restful-react'
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import { useModalHook } from 'hooks/useModalHook'
|
||||||
|
import { useStrings } from 'framework/strings'
|
||||||
|
import type { EnumTriggerAction, OpenapiCreateTriggerRequest, TypesTrigger } from 'services/code'
|
||||||
|
import { getErrorMessage } from 'utils/Utils'
|
||||||
|
import { triggerActions } from 'components/PipelineTriggersTab/PipelineTriggersTab'
|
||||||
|
import css from './NewTriggerModalButton.module.scss'
|
||||||
|
|
||||||
|
export interface TriggerFormData {
|
||||||
|
name: string
|
||||||
|
actions: EnumTriggerAction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const formInitialValues: TriggerFormData = {
|
||||||
|
name: '',
|
||||||
|
actions: []
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewTriggerModalButtonProps extends Omit<ButtonProps, 'onClick' | 'onSubmit'> {
|
||||||
|
repoPath: string
|
||||||
|
pipeline: string
|
||||||
|
modalTitle: string
|
||||||
|
submitButtonTitle?: string
|
||||||
|
cancelButtonTitle?: string
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NewTriggerModalButton: React.FC<NewTriggerModalButtonProps> = ({
|
||||||
|
repoPath,
|
||||||
|
pipeline,
|
||||||
|
modalTitle,
|
||||||
|
submitButtonTitle,
|
||||||
|
cancelButtonTitle,
|
||||||
|
onSuccess,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const ModalComponent: React.FC = () => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { showError, showSuccess } = useToaster()
|
||||||
|
|
||||||
|
const { mutate: createTrigger, loading } = useMutate<TypesTrigger>({
|
||||||
|
verb: 'POST',
|
||||||
|
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}/triggers`
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = async (formData: TriggerFormData) => {
|
||||||
|
try {
|
||||||
|
const payload: OpenapiCreateTriggerRequest = {
|
||||||
|
actions: formData.actions,
|
||||||
|
uid: formData.name
|
||||||
|
}
|
||||||
|
await createTrigger(payload)
|
||||||
|
hideModal()
|
||||||
|
showSuccess(getString('triggers.createSuccess'))
|
||||||
|
onSuccess()
|
||||||
|
} catch (exception) {
|
||||||
|
showError(getErrorMessage(exception), 0, getString('triggers.failedToCreate'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
isOpen
|
||||||
|
enforceFocus={false}
|
||||||
|
onClose={hideModal}
|
||||||
|
title={modalTitle}
|
||||||
|
style={{ width: 700, maxHeight: '95vh', overflow: 'auto' }}>
|
||||||
|
<Layout.Vertical padding={'large'} style={{ height: '100%' }} data-testid="add-trigger-modal">
|
||||||
|
<Container>
|
||||||
|
<Formik
|
||||||
|
initialValues={formInitialValues}
|
||||||
|
formName="addTrigger"
|
||||||
|
enableReinitialize={true}
|
||||||
|
validationSchema={yup.object().shape({
|
||||||
|
name: yup
|
||||||
|
.string()
|
||||||
|
.required('name is required')
|
||||||
|
.matches(
|
||||||
|
/^[a-zA-Z_][a-zA-Z0-9-_.]*$/,
|
||||||
|
'name must start with a letter or _ and only contain [a-zA-Z0-9-_.]'
|
||||||
|
),
|
||||||
|
actions: yup.array().of(yup.string())
|
||||||
|
})}
|
||||||
|
validateOnChange
|
||||||
|
validateOnBlur
|
||||||
|
onSubmit={handleSubmit}>
|
||||||
|
{formik => (
|
||||||
|
<FormikForm>
|
||||||
|
<FormInput.Text
|
||||||
|
name="name"
|
||||||
|
label={getString('name')}
|
||||||
|
placeholder={getString('triggers.enterTriggerName')}
|
||||||
|
inputGroup={{ autoFocus: true }}
|
||||||
|
/>
|
||||||
|
<Text font={{ variation: FontVariation.FORM_LABEL }} margin={{ bottom: 'xsmall' }}>
|
||||||
|
{getString('triggers.actions')}
|
||||||
|
</Text>
|
||||||
|
<Container className={css.actionsContainer} padding={'large'}>
|
||||||
|
{triggerActions.map(action => (
|
||||||
|
<Checkbox
|
||||||
|
key={action.name}
|
||||||
|
name="actions"
|
||||||
|
label={action.name}
|
||||||
|
value={action.value}
|
||||||
|
onChange={event => {
|
||||||
|
if (event.currentTarget.checked) {
|
||||||
|
formik.setFieldValue('actions', [...formik.values.actions, action.value])
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'actions',
|
||||||
|
formik.values.actions.filter((value: string) => value !== action.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
<Layout.Horizontal spacing="small" padding={{ top: 'large' }} style={{ alignItems: 'center' }}>
|
||||||
|
<Button type="submit" text={getString('create')} intent={Intent.PRIMARY} disabled={loading} />
|
||||||
|
<Button text={cancelButtonTitle || getString('cancel')} minimal onClick={hideModal} />
|
||||||
|
<FlexExpander />
|
||||||
|
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</FormikForm>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</Container>
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [openModal, hideModal] = useModalHook(ModalComponent, [onSuccess])
|
||||||
|
|
||||||
|
return <Button onClick={openModal} {...props} />
|
||||||
|
}
|
@ -7,22 +7,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.generalContainer {
|
|
||||||
width: 100%;
|
|
||||||
background: var(--grey-0) !important;
|
|
||||||
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yellowContainer {
|
|
||||||
background: var(--yellow-100) !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textContainer {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.withError {
|
.withError {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// This is an auto-generated file
|
// This is an auto-generated file
|
||||||
export declare const generalContainer: string
|
|
||||||
export declare const layout: string
|
export declare const layout: string
|
||||||
export declare const main: string
|
export declare const main: string
|
||||||
export declare const textContainer: string
|
|
||||||
export declare const withError: string
|
export declare const withError: string
|
||||||
export declare const yellowContainer: string
|
|
||||||
|
@ -1,29 +1,17 @@
|
|||||||
import {
|
import { Container, PageBody } from '@harnessio/uicore'
|
||||||
Button,
|
|
||||||
ButtonVariation,
|
|
||||||
Container,
|
|
||||||
FormInput,
|
|
||||||
Formik,
|
|
||||||
FormikForm,
|
|
||||||
Layout,
|
|
||||||
PageBody,
|
|
||||||
Text,
|
|
||||||
useToaster
|
|
||||||
} from '@harnessio/uicore'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useHistory, useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { Color, Intent } from '@harnessio/design-system'
|
import { useGet } from 'restful-react'
|
||||||
import { useGet, useMutate } from 'restful-react'
|
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import * as yup from 'yup'
|
|
||||||
import PipelineSettingsPageHeader from 'components/PipelineSettingsPageHeader/PipelineSettingsPageHeader'
|
import PipelineSettingsPageHeader from 'components/PipelineSettingsPageHeader/PipelineSettingsPageHeader'
|
||||||
import { String, useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||||
import { routes, type CODEProps } from 'RouteDefinitions'
|
import { routes, type CODEProps } from 'RouteDefinitions'
|
||||||
import { useConfirmAct } from 'hooks/useConfirmAction'
|
|
||||||
import { getErrorMessage, voidFn } from 'utils/Utils'
|
import { getErrorMessage, voidFn } from 'utils/Utils'
|
||||||
import type { OpenapiUpdatePipelineRequest, TypesPipeline } from 'services/code'
|
import type { TypesPipeline } from 'services/code'
|
||||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||||
|
import PipelineSettingsTab from 'components/PipelineSettingsTab/PipelineSettingsTab'
|
||||||
|
import PipelineTriggersTabs from 'components/PipelineTriggersTab/PipelineTriggersTab'
|
||||||
import css from './PipelineSettings.module.scss'
|
import css from './PipelineSettings.module.scss'
|
||||||
|
|
||||||
export enum TabOptions {
|
export enum TabOptions {
|
||||||
@ -31,163 +19,6 @@ export enum TabOptions {
|
|||||||
TRIGGERS = 'Triggers'
|
TRIGGERS = 'Triggers'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingsContentProps {
|
|
||||||
pipeline: string
|
|
||||||
repoPath: string
|
|
||||||
yamlPath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SettingsFormData {
|
|
||||||
name: string
|
|
||||||
yamlPath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsContent = ({ pipeline, repoPath, yamlPath }: SettingsContentProps) => {
|
|
||||||
const { getString } = useStrings()
|
|
||||||
const { mutate: updatePipeline } = useMutate<TypesPipeline>({
|
|
||||||
verb: 'PATCH',
|
|
||||||
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`
|
|
||||||
})
|
|
||||||
const { mutate: deletePipeline } = useMutate<TypesPipeline>({
|
|
||||||
verb: 'DELETE',
|
|
||||||
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`
|
|
||||||
})
|
|
||||||
const { showSuccess, showError } = useToaster()
|
|
||||||
const confirmDeletePipeline = useConfirmAct()
|
|
||||||
const history = useHistory()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout.Vertical padding={'medium'} spacing={'medium'}>
|
|
||||||
<Container padding={'large'} className={css.generalContainer}>
|
|
||||||
<Formik<SettingsFormData>
|
|
||||||
initialValues={{
|
|
||||||
name: pipeline,
|
|
||||||
yamlPath
|
|
||||||
}}
|
|
||||||
formName="pipelineSettings"
|
|
||||||
enableReinitialize={true}
|
|
||||||
validationSchema={yup.object().shape({
|
|
||||||
name: yup
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.required(`${getString('name')} ${getString('isRequired')}`),
|
|
||||||
yamlPath: yup
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.required(`${getString('pipelines.yamlPath')} ${getString('isRequired')}`)
|
|
||||||
})}
|
|
||||||
validateOnChange
|
|
||||||
validateOnBlur
|
|
||||||
onSubmit={async formData => {
|
|
||||||
const { name, yamlPath: newYamlPath } = formData
|
|
||||||
try {
|
|
||||||
const payload: OpenapiUpdatePipelineRequest = {
|
|
||||||
config_path: newYamlPath,
|
|
||||||
uid: name
|
|
||||||
}
|
|
||||||
await updatePipeline(payload, {
|
|
||||||
pathParams: { path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}` }
|
|
||||||
})
|
|
||||||
history.push(
|
|
||||||
routes.toCODEPipelineSettings({
|
|
||||||
repoPath,
|
|
||||||
pipeline: name
|
|
||||||
})
|
|
||||||
)
|
|
||||||
showSuccess(getString('pipelines.updatePipelineSuccess', { pipeline }))
|
|
||||||
} catch (exception) {
|
|
||||||
showError(getErrorMessage(exception), 0, 'pipelines.failedToUpdatePipeline')
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{() => {
|
|
||||||
return (
|
|
||||||
<FormikForm>
|
|
||||||
<Layout.Vertical spacing={'large'}>
|
|
||||||
<Layout.Horizontal spacing={'large'} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
|
|
||||||
<FormInput.Text
|
|
||||||
name="name"
|
|
||||||
className={css.textContainer}
|
|
||||||
label={
|
|
||||||
<Text color={Color.GREY_800} font={{ size: 'small' }}>
|
|
||||||
{getString('name')}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
<Layout.Horizontal spacing={'large'} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
|
|
||||||
<FormInput.Text
|
|
||||||
name="yamlPath"
|
|
||||||
className={css.textContainer}
|
|
||||||
label={
|
|
||||||
<Text color={Color.GREY_800} font={{ size: 'small' }}>
|
|
||||||
{getString('pipelines.yamlPath')}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
<Layout.Horizontal spacing={'large'}>
|
|
||||||
<Button intent={Intent.PRIMARY} type="submit" text={getString('save')} />
|
|
||||||
<Button variation={ButtonVariation.TERTIARY} type="reset" text={getString('cancel')} />
|
|
||||||
</Layout.Horizontal>
|
|
||||||
</Layout.Vertical>
|
|
||||||
</FormikForm>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Formik>
|
|
||||||
</Container>
|
|
||||||
<Container padding={'large'} className={css.generalContainer}>
|
|
||||||
<Layout.Vertical>
|
|
||||||
<Text icon="main-trash" color={Color.GREY_600} font={{ size: 'normal' }}>
|
|
||||||
{getString('dangerDeleteRepo')}
|
|
||||||
</Text>
|
|
||||||
<Layout.Horizontal padding={{ top: 'medium', left: 'medium' }} flex={{ justifyContent: 'space-between' }}>
|
|
||||||
<Container intent="warning" padding={'small'} className={css.yellowContainer}>
|
|
||||||
<Text
|
|
||||||
icon="main-issue"
|
|
||||||
iconProps={{ size: 18, color: Color.ORANGE_700, margin: { right: 'small' } }}
|
|
||||||
color={Color.WARNING}>
|
|
||||||
{getString('pipelines.deletePipelineWarning', {
|
|
||||||
pipeline
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Container>
|
|
||||||
<Button
|
|
||||||
margin={{ right: 'medium' }}
|
|
||||||
intent={Intent.DANGER}
|
|
||||||
onClick={() => {
|
|
||||||
confirmDeletePipeline({
|
|
||||||
title: getString('pipelines.deletePipelineButton'),
|
|
||||||
confirmText: getString('delete'),
|
|
||||||
intent: Intent.DANGER,
|
|
||||||
message: <String useRichText stringID="pipelines.deletePipelineConfirm" vars={{ pipeline }} />,
|
|
||||||
action: async () => {
|
|
||||||
try {
|
|
||||||
await deletePipeline(null)
|
|
||||||
history.push(
|
|
||||||
routes.toCODEPipelines({
|
|
||||||
repoPath
|
|
||||||
})
|
|
||||||
)
|
|
||||||
showSuccess(getString('pipelines.deletePipelineSuccess', { pipeline }))
|
|
||||||
} catch (e) {
|
|
||||||
showError(getString('pipelines.deletePipelineError'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
variation={ButtonVariation.PRIMARY}
|
|
||||||
text={getString('pipelines.deletePipelineButton')}></Button>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
</Layout.Vertical>
|
|
||||||
</Container>
|
|
||||||
</Layout.Vertical>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const TriggersContent = () => {
|
|
||||||
return <div>Triggers</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const PipelineSettings = () => {
|
const PipelineSettings = () => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
|
|
||||||
@ -232,13 +63,15 @@ const PipelineSettings = () => {
|
|||||||
retryOnError={voidFn(refetch)}>
|
retryOnError={voidFn(refetch)}>
|
||||||
<LoadingSpinner visible={loading || pipelineLoading} withBorder={!!pipeline} />
|
<LoadingSpinner visible={loading || pipelineLoading} withBorder={!!pipeline} />
|
||||||
{selectedTab === TabOptions.SETTINGS && (
|
{selectedTab === TabOptions.SETTINGS && (
|
||||||
<SettingsContent
|
<PipelineSettingsTab
|
||||||
pipeline={pipeline as string}
|
pipeline={pipeline as string}
|
||||||
repoPath={repoMetadata?.path as string}
|
repoPath={repoMetadata?.path as string}
|
||||||
yamlPath={pipelineData?.config_path as string}
|
yamlPath={pipelineData?.config_path as string}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{selectedTab === TabOptions.TRIGGERS && <TriggersContent />}
|
{selectedTab === TabOptions.TRIGGERS && (
|
||||||
|
<PipelineTriggersTabs pipeline={pipeline as string} repoPath={repoMetadata?.path as string} />
|
||||||
|
)}
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
.generalContainer {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--grey-0) !important;
|
||||||
|
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellowContainer {
|
||||||
|
background: var(--yellow-100) !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textContainer {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 1px solid rgba(217, 218, 229, 0.2) !important;
|
||||||
|
}
|
6
web/src/components/PipelineSettingsTab/PipelineSettingsTab.module.scss.d.ts
vendored
Normal file
6
web/src/components/PipelineSettingsTab/PipelineSettingsTab.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// This is an auto-generated file
|
||||||
|
export declare const generalContainer: string
|
||||||
|
export declare const separator: string
|
||||||
|
export declare const textContainer: string
|
||||||
|
export declare const yellowContainer: string
|
178
web/src/components/PipelineSettingsTab/PipelineSettingsTab.tsx
Normal file
178
web/src/components/PipelineSettingsTab/PipelineSettingsTab.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonVariation,
|
||||||
|
Container,
|
||||||
|
FormInput,
|
||||||
|
Formik,
|
||||||
|
FormikForm,
|
||||||
|
Layout,
|
||||||
|
Text,
|
||||||
|
useToaster
|
||||||
|
} from '@harnessio/uicore'
|
||||||
|
import React from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { Color, Intent } from '@harnessio/design-system'
|
||||||
|
import { useMutate } from 'restful-react'
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import { String, useStrings } from 'framework/strings'
|
||||||
|
import { routes } from 'RouteDefinitions'
|
||||||
|
import { useConfirmAct } from 'hooks/useConfirmAction'
|
||||||
|
import { getErrorMessage } from 'utils/Utils'
|
||||||
|
import type { OpenapiUpdatePipelineRequest, TypesPipeline } from 'services/code'
|
||||||
|
import css from './PipelineSettingsTab.module.scss'
|
||||||
|
|
||||||
|
interface SettingsContentProps {
|
||||||
|
pipeline: string
|
||||||
|
repoPath: string
|
||||||
|
yamlPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsFormData {
|
||||||
|
name: string
|
||||||
|
yamlPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PipelineSettingsTab = ({ pipeline, repoPath, yamlPath }: SettingsContentProps) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { mutate: updatePipeline } = useMutate<TypesPipeline>({
|
||||||
|
verb: 'PATCH',
|
||||||
|
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`
|
||||||
|
})
|
||||||
|
const { mutate: deletePipeline } = useMutate<TypesPipeline>({
|
||||||
|
verb: 'DELETE',
|
||||||
|
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`
|
||||||
|
})
|
||||||
|
const { showSuccess, showError } = useToaster()
|
||||||
|
const confirmDeletePipeline = useConfirmAct()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Vertical padding={'medium'} spacing={'medium'}>
|
||||||
|
<Container padding={'large'} className={css.generalContainer}>
|
||||||
|
<Formik<SettingsFormData>
|
||||||
|
initialValues={{
|
||||||
|
name: pipeline,
|
||||||
|
yamlPath
|
||||||
|
}}
|
||||||
|
formName="pipelineSettings"
|
||||||
|
enableReinitialize={true}
|
||||||
|
validationSchema={yup.object().shape({
|
||||||
|
name: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.required(`${getString('name')} ${getString('isRequired')}`),
|
||||||
|
yamlPath: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.required(`${getString('pipelines.yamlPath')} ${getString('isRequired')}`)
|
||||||
|
})}
|
||||||
|
validateOnChange
|
||||||
|
validateOnBlur
|
||||||
|
onSubmit={async formData => {
|
||||||
|
const { name, yamlPath: newYamlPath } = formData
|
||||||
|
try {
|
||||||
|
const payload: OpenapiUpdatePipelineRequest = {
|
||||||
|
config_path: newYamlPath,
|
||||||
|
uid: name
|
||||||
|
}
|
||||||
|
await updatePipeline(payload, {
|
||||||
|
pathParams: { path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}` }
|
||||||
|
})
|
||||||
|
history.push(
|
||||||
|
routes.toCODEPipelineSettings({
|
||||||
|
repoPath,
|
||||||
|
pipeline: name
|
||||||
|
})
|
||||||
|
)
|
||||||
|
showSuccess(getString('pipelines.updatePipelineSuccess', { pipeline }))
|
||||||
|
} catch (exception) {
|
||||||
|
showError(getErrorMessage(exception), 0, 'pipelines.failedToUpdatePipeline')
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<FormikForm>
|
||||||
|
<Layout.Vertical spacing={'large'}>
|
||||||
|
<Layout.Horizontal spacing={'large'} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
|
||||||
|
<FormInput.Text
|
||||||
|
name="name"
|
||||||
|
className={css.textContainer}
|
||||||
|
label={
|
||||||
|
<Text color={Color.GREY_800} font={{ size: 'small' }}>
|
||||||
|
{getString('name')}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
<Layout.Horizontal spacing={'large'} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
|
||||||
|
<FormInput.Text
|
||||||
|
name="yamlPath"
|
||||||
|
className={css.textContainer}
|
||||||
|
label={
|
||||||
|
<Text color={Color.GREY_800} font={{ size: 'small' }}>
|
||||||
|
{getString('pipelines.yamlPath')}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
<div className={css.separator} />
|
||||||
|
<Layout.Horizontal spacing={'large'}>
|
||||||
|
<Button intent={Intent.PRIMARY} type="submit" text={getString('save')} />
|
||||||
|
<Button variation={ButtonVariation.TERTIARY} type="reset" text={getString('cancel')} />
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Layout.Vertical>
|
||||||
|
</FormikForm>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
</Container>
|
||||||
|
<Container padding={'large'} className={css.generalContainer}>
|
||||||
|
<Layout.Vertical>
|
||||||
|
<Text icon="main-trash" color={Color.GREY_600} font={{ size: 'normal' }}>
|
||||||
|
{getString('dangerDeleteRepo')}
|
||||||
|
</Text>
|
||||||
|
<Layout.Horizontal padding={{ top: 'medium', left: 'medium' }} flex={{ justifyContent: 'space-between' }}>
|
||||||
|
<Container intent="warning" padding={'small'} className={css.yellowContainer}>
|
||||||
|
<Text
|
||||||
|
icon="main-issue"
|
||||||
|
iconProps={{ size: 18, color: Color.ORANGE_700, margin: { right: 'small' } }}
|
||||||
|
color={Color.WARNING}>
|
||||||
|
{getString('pipelines.deletePipelineWarning', {
|
||||||
|
pipeline
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
<Button
|
||||||
|
margin={{ right: 'medium' }}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClick={() => {
|
||||||
|
confirmDeletePipeline({
|
||||||
|
title: getString('pipelines.deletePipelineButton'),
|
||||||
|
confirmText: getString('delete'),
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: <String useRichText stringID="pipelines.deletePipelineConfirm" vars={{ pipeline }} />,
|
||||||
|
action: async () => {
|
||||||
|
try {
|
||||||
|
await deletePipeline(null)
|
||||||
|
history.push(
|
||||||
|
routes.toCODEPipelines({
|
||||||
|
repoPath
|
||||||
|
})
|
||||||
|
)
|
||||||
|
showSuccess(getString('pipelines.deletePipelineSuccess', { pipeline }))
|
||||||
|
} catch (e) {
|
||||||
|
showError(getString('pipelines.deletePipelineError'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
variation={ButtonVariation.PRIMARY}
|
||||||
|
text={getString('pipelines.deletePipelineButton')}></Button>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Container>
|
||||||
|
</Layout.Vertical>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PipelineSettingsTab
|
@ -0,0 +1,65 @@
|
|||||||
|
.separator {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 1px solid rgba(217, 218, 229, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generalContainer {
|
||||||
|
background: var(--grey-0) !important;
|
||||||
|
box-shadow: 0px 0.5px 2px 0px rgba(96, 97, 112, 0.16), 0px 0px 1px 0px rgba(40, 41, 61, 0.08) !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
margin: 1px 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TriggerMenuItem {
|
||||||
|
width: 500px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editTriggerContainer {
|
||||||
|
width: 800px !important;
|
||||||
|
margin-top: 53px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
border: 1px solid var(--primary-7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillContainer {
|
||||||
|
background-color: var(--grey-100) !important;
|
||||||
|
color: var(--grey-400) !important;
|
||||||
|
padding: 4px !important;
|
||||||
|
height: 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillText {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerName {
|
||||||
|
color: var(--gray-scale-700) !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerDate {
|
||||||
|
color: var(--gray-scale-500) !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerList {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editTriggerTitle {
|
||||||
|
color: var(--gray-scale-700);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsContainer {
|
||||||
|
width: 100% !important;
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
gap: 10px !important;
|
||||||
|
}
|
14
web/src/components/PipelineTriggersTab/PipelineTriggersTab.module.scss.d.ts
vendored
Normal file
14
web/src/components/PipelineTriggersTab/PipelineTriggersTab.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// This is an auto-generated file
|
||||||
|
export declare const actionsContainer: string
|
||||||
|
export declare const editTriggerContainer: string
|
||||||
|
export declare const editTriggerTitle: string
|
||||||
|
export declare const generalContainer: string
|
||||||
|
export declare const pillContainer: string
|
||||||
|
export declare const pillText: string
|
||||||
|
export declare const selected: string
|
||||||
|
export declare const separator: string
|
||||||
|
export declare const triggerDate: string
|
||||||
|
export declare const triggerList: string
|
||||||
|
export declare const triggerMenuItem: string
|
||||||
|
export declare const triggerName: string
|
302
web/src/components/PipelineTriggersTab/PipelineTriggersTab.tsx
Normal file
302
web/src/components/PipelineTriggersTab/PipelineTriggersTab.tsx
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import {
|
||||||
|
ButtonVariation,
|
||||||
|
FlexExpander,
|
||||||
|
Layout,
|
||||||
|
Text,
|
||||||
|
Formik,
|
||||||
|
FormikForm,
|
||||||
|
Container,
|
||||||
|
Button,
|
||||||
|
useToaster,
|
||||||
|
Checkbox
|
||||||
|
} from '@harnessio/uicore'
|
||||||
|
import React from 'react'
|
||||||
|
import { useGet, useMutate } from 'restful-react'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import { FontVariation, Intent } from '@harnessio/design-system'
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import { Icon } from '@harnessio/icons'
|
||||||
|
import { String, useStrings } from 'framework/strings'
|
||||||
|
import type { EnumTriggerAction, OpenapiUpdateTriggerRequest, TypesTrigger } from 'services/code'
|
||||||
|
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||||
|
import { NewTriggerModalButton } from 'components/NewTriggerModalButton/NewTriggerModalButton'
|
||||||
|
import { getErrorMessage } from 'utils/Utils'
|
||||||
|
import { useConfirmAct } from 'hooks/useConfirmAction'
|
||||||
|
import css from './PipelineTriggersTab.module.scss'
|
||||||
|
|
||||||
|
type TriggerAction = {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const triggerActions: TriggerAction[] = [
|
||||||
|
{ name: 'Branch Created', value: 'branch_created' },
|
||||||
|
{ name: 'Branch Updated', value: 'branch_updated' },
|
||||||
|
{ name: 'Pull Request Branch Updated', value: 'pullreq_branch_updated' },
|
||||||
|
{ name: 'Pull Request Created', value: 'pullreq_created' },
|
||||||
|
{ name: 'Pull Request Reopened', value: 'pullreq_reopened' },
|
||||||
|
{ name: 'Tag Created', value: 'tag_created' },
|
||||||
|
{ name: 'Tag Updated', value: 'tag_updated' }
|
||||||
|
]
|
||||||
|
|
||||||
|
interface TriggerMenuItemProps {
|
||||||
|
name: string
|
||||||
|
lastUpdated: number
|
||||||
|
index: number
|
||||||
|
setSelectedTrigger: (trigger: number) => void
|
||||||
|
isSelected?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const TriggerMenuItem = ({ name, lastUpdated, setSelectedTrigger, index, isSelected }: TriggerMenuItemProps) => {
|
||||||
|
return (
|
||||||
|
<Layout.Horizontal
|
||||||
|
spacing={'medium'}
|
||||||
|
className={cx(css.generalContainer, css.triggerMenuItem, { [css.selected]: isSelected })}
|
||||||
|
flex
|
||||||
|
padding={'large'}
|
||||||
|
onClick={() => setSelectedTrigger(index)}>
|
||||||
|
<Layout.Vertical spacing={'small'}>
|
||||||
|
<Text className={css.triggerName}>{name}</Text>
|
||||||
|
<Text className={css.triggerDate}>{`Last update: ${new Date(lastUpdated).toLocaleDateString('en-US', {
|
||||||
|
month: 'short', // abbreviated month name
|
||||||
|
day: '2-digit', // two-digit day
|
||||||
|
year: 'numeric' // four-digit year
|
||||||
|
})}`}</Text>
|
||||||
|
</Layout.Vertical>
|
||||||
|
<FlexExpander />
|
||||||
|
<Layout.Horizontal
|
||||||
|
spacing={'xsmall'}
|
||||||
|
style={{ alignItems: 'center', borderRadius: '4px' }}
|
||||||
|
className={css.pillContainer}>
|
||||||
|
<Text className={css.pillText} font={{ size: 'xsmall' }}>
|
||||||
|
Internal
|
||||||
|
</Text>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TriggerDetailsProps {
|
||||||
|
name: string
|
||||||
|
repoPath: string
|
||||||
|
pipeline: string
|
||||||
|
refetchTriggers: () => void
|
||||||
|
setSelectedTrigger: (trigger: number) => void
|
||||||
|
initialDisabled: boolean
|
||||||
|
initialActions: EnumTriggerAction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TriggerFormData {
|
||||||
|
disabled: boolean
|
||||||
|
actions: EnumTriggerAction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const TriggerDetails = ({
|
||||||
|
name,
|
||||||
|
repoPath,
|
||||||
|
pipeline,
|
||||||
|
refetchTriggers,
|
||||||
|
setSelectedTrigger,
|
||||||
|
initialActions,
|
||||||
|
initialDisabled
|
||||||
|
}: TriggerDetailsProps) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { showError, showSuccess } = useToaster()
|
||||||
|
|
||||||
|
const { mutate: updateTrigger, loading } = useMutate<TypesTrigger>({
|
||||||
|
verb: 'PATCH',
|
||||||
|
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}/triggers/${name}`
|
||||||
|
})
|
||||||
|
const { mutate: deleteTrigger } = useMutate<TypesTrigger>({
|
||||||
|
verb: 'DELETE',
|
||||||
|
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}/triggers/${name}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const confirmDeleteTrigger = useConfirmAct()
|
||||||
|
|
||||||
|
const handleSubmit = async (formData: TriggerFormData) => {
|
||||||
|
try {
|
||||||
|
const payload: OpenapiUpdateTriggerRequest = {
|
||||||
|
actions: formData.actions,
|
||||||
|
disabled: formData.disabled
|
||||||
|
}
|
||||||
|
await updateTrigger(payload)
|
||||||
|
showSuccess(getString('triggers.updateSuccess'))
|
||||||
|
refetchTriggers()
|
||||||
|
} catch (exception) {
|
||||||
|
showError(getErrorMessage(exception), 0, getString('triggers.failedToUpdate'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formInitialValues: TriggerFormData = {
|
||||||
|
actions: initialActions,
|
||||||
|
disabled: initialDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Vertical className={cx(css.generalContainer, css.editTriggerContainer)} padding={'large'}>
|
||||||
|
<Layout.Horizontal padding={{ top: 'medium', left: 'large', right: 'large' }}>
|
||||||
|
<Text font={{ variation: FontVariation.H5 }} className={css.editTriggerTitle}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<FlexExpander />
|
||||||
|
<Layout.Horizontal
|
||||||
|
spacing={'xsmall'}
|
||||||
|
style={{ alignItems: 'center', borderRadius: '4px' }}
|
||||||
|
className={css.pillContainer}>
|
||||||
|
<Text className={css.pillText} font={{ size: 'xsmall' }}>
|
||||||
|
Internal
|
||||||
|
</Text>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
<Formik
|
||||||
|
initialValues={formInitialValues}
|
||||||
|
formName="editTrigger"
|
||||||
|
enableReinitialize={true}
|
||||||
|
validationSchema={yup.object().shape({
|
||||||
|
actions: yup.array().of(yup.string()),
|
||||||
|
disabled: yup.boolean()
|
||||||
|
})}
|
||||||
|
validateOnChange
|
||||||
|
validateOnBlur
|
||||||
|
onSubmit={handleSubmit}>
|
||||||
|
{formik => (
|
||||||
|
<FormikForm>
|
||||||
|
<Container padding={'large'}>
|
||||||
|
<Checkbox
|
||||||
|
name="disabled"
|
||||||
|
label={getString('triggers.disableTrigger')}
|
||||||
|
checked={formik.values.disabled}
|
||||||
|
onChange={event => {
|
||||||
|
if (event.currentTarget.checked) {
|
||||||
|
formik.setFieldValue('disabled', true)
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue('disabled', false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
<div className={css.separator} />
|
||||||
|
<Container className={css.actionsContainer} padding={'large'}>
|
||||||
|
{triggerActions.map(action => (
|
||||||
|
<Checkbox
|
||||||
|
key={action.name}
|
||||||
|
name="actions"
|
||||||
|
label={action.name}
|
||||||
|
value={action.value}
|
||||||
|
checked={formik.values.actions.includes(action.value as EnumTriggerAction)}
|
||||||
|
onChange={event => {
|
||||||
|
if (event.currentTarget.checked) {
|
||||||
|
formik.setFieldValue('actions', [...formik.values.actions, action.value])
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'actions',
|
||||||
|
formik.values.actions.filter((value: string) => value !== action.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
<div className={css.separator} />
|
||||||
|
<Layout.Horizontal
|
||||||
|
spacing="small"
|
||||||
|
padding={{ top: 'large', left: 'large', right: 'large' }}
|
||||||
|
style={{ alignItems: 'center' }}>
|
||||||
|
<Button type="submit" text={getString('edit')} intent={Intent.PRIMARY} disabled={loading} />
|
||||||
|
<Button
|
||||||
|
text={getString('triggers.deleteTrigger')}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
variation={ButtonVariation.SECONDARY}
|
||||||
|
onClick={() => {
|
||||||
|
confirmDeleteTrigger({
|
||||||
|
title: getString('triggers.deleteTrigger'),
|
||||||
|
confirmText: getString('delete'),
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: <String useRichText stringID="triggers.deleteTriggerConfirm" vars={{ name }} />,
|
||||||
|
action: async () => {
|
||||||
|
try {
|
||||||
|
await deleteTrigger(null)
|
||||||
|
refetchTriggers()
|
||||||
|
setSelectedTrigger(0)
|
||||||
|
showSuccess(getString('triggers.deleteTriggerSuccess', { name }))
|
||||||
|
} catch (e) {
|
||||||
|
showError(getString('triggers.deleteTriggerError'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FlexExpander />
|
||||||
|
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</FormikForm>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</Layout.Vertical>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PipelineTriggersTabsProps {
|
||||||
|
pipeline: string
|
||||||
|
repoPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PipelineTriggersTabs = ({ repoPath, pipeline }: PipelineTriggersTabsProps) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
|
||||||
|
const { data, loading, refetch } = useGet<TypesTrigger[]>({
|
||||||
|
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}/triggers`,
|
||||||
|
lazy: !repoPath || !pipeline
|
||||||
|
})
|
||||||
|
|
||||||
|
const [selectedTrigger, setSelectedTrigger] = React.useState<number>(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LoadingSpinner visible={loading} />
|
||||||
|
{data?.length && (
|
||||||
|
<Layout.Horizontal padding={'large'}>
|
||||||
|
<Layout.Vertical padding={'large'}>
|
||||||
|
<NewTriggerModalButton
|
||||||
|
modalTitle={getString('triggers.createTrigger')}
|
||||||
|
text={getString('triggers.newTrigger')}
|
||||||
|
variation={ButtonVariation.PRIMARY}
|
||||||
|
icon="plus"
|
||||||
|
onSuccess={() => refetch()}
|
||||||
|
repoPath={repoPath}
|
||||||
|
pipeline={pipeline}
|
||||||
|
width="150px"
|
||||||
|
/>
|
||||||
|
<Layout.Vertical spacing={'large'} className={css.triggerList}>
|
||||||
|
{data?.map((trigger, index) => (
|
||||||
|
<TriggerMenuItem
|
||||||
|
key={trigger.id}
|
||||||
|
name={trigger.uid as string}
|
||||||
|
lastUpdated={trigger.updated as number}
|
||||||
|
setSelectedTrigger={setSelectedTrigger}
|
||||||
|
index={index}
|
||||||
|
isSelected={selectedTrigger === index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Layout.Vertical>
|
||||||
|
<div className={css.separator} />
|
||||||
|
<Layout.Vertical padding={'large'}>
|
||||||
|
<TriggerDetails
|
||||||
|
name={data?.[selectedTrigger]?.uid as string}
|
||||||
|
repoPath={repoPath}
|
||||||
|
pipeline={pipeline}
|
||||||
|
refetchTriggers={refetch}
|
||||||
|
setSelectedTrigger={setSelectedTrigger}
|
||||||
|
initialActions={data?.[selectedTrigger]?.actions as EnumTriggerAction[]}
|
||||||
|
initialDisabled={data?.[selectedTrigger]?.disabled as boolean}
|
||||||
|
/>
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PipelineTriggersTabs
|
@ -597,6 +597,19 @@ export interface StringsMap {
|
|||||||
title: string
|
title: string
|
||||||
token: string
|
token: string
|
||||||
tooltipRepoEdit: string
|
tooltipRepoEdit: string
|
||||||
|
'triggers.actions': string
|
||||||
|
'triggers.createSuccess': string
|
||||||
|
'triggers.createTrigger': string
|
||||||
|
'triggers.deleteTrigger': string
|
||||||
|
'triggers.deleteTriggerConfirm': string
|
||||||
|
'triggers.deleteTriggerError': string
|
||||||
|
'triggers.deleteTriggerSuccess': string
|
||||||
|
'triggers.disableTrigger': string
|
||||||
|
'triggers.enterTriggerName': string
|
||||||
|
'triggers.failedToCreate': string
|
||||||
|
'triggers.failedToUpdate': string
|
||||||
|
'triggers.newTrigger': string
|
||||||
|
'triggers.updateSuccess': string
|
||||||
unrsolvedComment: string
|
unrsolvedComment: string
|
||||||
'unsavedChanges.leave': string
|
'unsavedChanges.leave': string
|
||||||
'unsavedChanges.message': string
|
'unsavedChanges.message': string
|
||||||
|
@ -730,5 +730,19 @@ importSpace:
|
|||||||
githubOrg: GitHub Organization Name
|
githubOrg: GitHub Organization Name
|
||||||
gitlabGroup: GitLab Group Name
|
gitlabGroup: GitLab Group Name
|
||||||
importProgress: 'Import in progress...'
|
importProgress: 'Import in progress...'
|
||||||
|
triggers:
|
||||||
|
newTrigger: New Trigger
|
||||||
|
createTrigger: Create a Trigger
|
||||||
|
createSuccess: Trigger created successfully
|
||||||
|
failedToCreate: Failed to create Trigger. Please try again.
|
||||||
|
enterTriggerName: Enter Trigger name
|
||||||
|
actions: Actions
|
||||||
|
updateSuccess: Trigger updated successfully
|
||||||
|
failedToUpdate: Failed to update Trigger. Please try again.
|
||||||
|
deleteTrigger: Delete trigger
|
||||||
|
disableTrigger: Disable trigger
|
||||||
|
deleteTriggerConfirm: Are you sure you want to delete trigger <strong>{{name}}</strong>? You can't undo this action.
|
||||||
|
deleteTriggerSuccess: Trigger {{name}} deleted.
|
||||||
|
deleteTriggerError: Failed to delete Trigger. Please try again.
|
||||||
step:
|
step:
|
||||||
select: Select a step
|
select: Select a step
|
||||||
|
@ -5,8 +5,6 @@ import { Get, GetProps, useGet, UseGetProps, Mutate, MutateProps, useMutate, Use
|
|||||||
|
|
||||||
import { getConfig } from '../config'
|
import { getConfig } from '../config'
|
||||||
export const SPEC_VERSION = '0.0.0'
|
export const SPEC_VERSION = '0.0.0'
|
||||||
export type EnumAccessGrant = number
|
|
||||||
|
|
||||||
export type EnumCIStatus = string
|
export type EnumCIStatus = string
|
||||||
|
|
||||||
export type EnumCheckPayloadKind = '' | 'markdown' | 'pipeline' | 'raw'
|
export type EnumCheckPayloadKind = '' | 'markdown' | 'pipeline' | 'raw'
|
||||||
@ -270,7 +268,6 @@ export interface OpenapiCreateTemplateRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenapiCreateTokenRequest {
|
export interface OpenapiCreateTokenRequest {
|
||||||
grants?: EnumAccessGrant
|
|
||||||
lifetime?: TimeDuration
|
lifetime?: TimeDuration
|
||||||
uid?: string
|
uid?: string
|
||||||
}
|
}
|
||||||
@ -278,7 +275,7 @@ export interface OpenapiCreateTokenRequest {
|
|||||||
export interface OpenapiCreateTriggerRequest {
|
export interface OpenapiCreateTriggerRequest {
|
||||||
actions?: EnumTriggerAction[] | null
|
actions?: EnumTriggerAction[] | null
|
||||||
description?: string
|
description?: string
|
||||||
enabled?: boolean
|
disabled?: boolean
|
||||||
secret?: string
|
secret?: string
|
||||||
uid?: string
|
uid?: string
|
||||||
}
|
}
|
||||||
@ -401,7 +398,7 @@ export interface OpenapiUpdateTemplateRequest {
|
|||||||
export interface OpenapiUpdateTriggerRequest {
|
export interface OpenapiUpdateTriggerRequest {
|
||||||
actions?: EnumTriggerAction[] | null
|
actions?: EnumTriggerAction[] | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
enabled?: boolean | null
|
disabled?: boolean | null
|
||||||
secret?: string | null
|
secret?: string | null
|
||||||
uid?: string | null
|
uid?: string | null
|
||||||
}
|
}
|
||||||
@ -660,6 +657,7 @@ export interface TypesPipeline {
|
|||||||
created_by?: number
|
created_by?: number
|
||||||
default_branch?: string
|
default_branch?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
disabled?: boolean
|
||||||
execution?: TypesExecution
|
execution?: TypesExecution
|
||||||
id?: number
|
id?: number
|
||||||
repo_id?: number
|
repo_id?: number
|
||||||
@ -779,6 +777,7 @@ export interface TypesRepository {
|
|||||||
|
|
||||||
export interface TypesSecret {
|
export interface TypesSecret {
|
||||||
created?: number
|
created?: number
|
||||||
|
created_by?: number
|
||||||
description?: string
|
description?: string
|
||||||
id?: number
|
id?: number
|
||||||
space_id?: number
|
space_id?: number
|
||||||
@ -872,7 +871,6 @@ export interface TypesTemplate {
|
|||||||
export interface TypesToken {
|
export interface TypesToken {
|
||||||
created_by?: number
|
created_by?: number
|
||||||
expires_at?: number | null
|
expires_at?: number | null
|
||||||
grants?: EnumAccessGrant
|
|
||||||
issued_at?: number
|
issued_at?: number
|
||||||
principal_id?: number
|
principal_id?: number
|
||||||
type?: EnumTokenType
|
type?: EnumTokenType
|
||||||
@ -889,10 +887,11 @@ export interface TypesTrigger {
|
|||||||
created?: number
|
created?: number
|
||||||
created_by?: number
|
created_by?: number
|
||||||
description?: string
|
description?: string
|
||||||
enabled?: boolean
|
disabled?: boolean
|
||||||
id?: number
|
id?: number
|
||||||
pipeline_id?: number
|
pipeline_id?: number
|
||||||
repo_id?: number
|
repo_id?: number
|
||||||
|
trigger_type?: string
|
||||||
uid?: string
|
uid?: string
|
||||||
updated?: number
|
updated?: number
|
||||||
}
|
}
|
||||||
|
@ -6745,8 +6745,6 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
OpenapiCreateTokenRequest:
|
OpenapiCreateTokenRequest:
|
||||||
properties:
|
properties:
|
||||||
grants:
|
|
||||||
$ref: '#/components/schemas/EnumAccessGrant'
|
|
||||||
lifetime:
|
lifetime:
|
||||||
$ref: '#/components/schemas/TimeDuration'
|
$ref: '#/components/schemas/TimeDuration'
|
||||||
uid:
|
uid:
|
||||||
@ -6761,7 +6759,7 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
enabled:
|
disabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
secret:
|
secret:
|
||||||
type: string
|
type: string
|
||||||
@ -6965,7 +6963,7 @@ components:
|
|||||||
description:
|
description:
|
||||||
nullable: true
|
nullable: true
|
||||||
type: string
|
type: string
|
||||||
enabled:
|
disabled:
|
||||||
nullable: true
|
nullable: true
|
||||||
type: boolean
|
type: boolean
|
||||||
secret:
|
secret:
|
||||||
@ -7414,6 +7412,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
|
disabled:
|
||||||
|
type: boolean
|
||||||
execution:
|
execution:
|
||||||
$ref: '#/components/schemas/TypesExecution'
|
$ref: '#/components/schemas/TypesExecution'
|
||||||
id:
|
id:
|
||||||
@ -7634,6 +7634,8 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
created:
|
created:
|
||||||
type: integer
|
type: integer
|
||||||
|
created_by:
|
||||||
|
type: integer
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
id:
|
id:
|
||||||
@ -7805,8 +7807,6 @@ components:
|
|||||||
expires_at:
|
expires_at:
|
||||||
nullable: true
|
nullable: true
|
||||||
type: integer
|
type: integer
|
||||||
grants:
|
|
||||||
$ref: '#/components/schemas/EnumAccessGrant'
|
|
||||||
issued_at:
|
issued_at:
|
||||||
type: integer
|
type: integer
|
||||||
principal_id:
|
principal_id:
|
||||||
@ -7836,7 +7836,7 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
enabled:
|
disabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
@ -7844,6 +7844,8 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
repo_id:
|
repo_id:
|
||||||
type: integer
|
type: integer
|
||||||
|
trigger_type:
|
||||||
|
type: string
|
||||||
uid:
|
uid:
|
||||||
type: string
|
type: string
|
||||||
updated:
|
updated:
|
||||||
|
Loading…
Reference in New Issue
Block a user