Compare commits
2 Commits
release/2.
...
feat1752-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58afb8be3d | ||
|
|
0e5fbf298e |
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/deploykey"
|
||||
"github.com/portainer/portainer/bolt/dockerhub"
|
||||
"github.com/portainer/portainer/bolt/endpoint"
|
||||
"github.com/portainer/portainer/bolt/endpointgroup"
|
||||
@@ -49,6 +50,7 @@ type Store struct {
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
DeploykeyService *deploykey.Service
|
||||
}
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
@@ -204,6 +206,12 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.TagService = tagService
|
||||
|
||||
deploykeyService, err := deploykey.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.DeploykeyService = deploykeyService
|
||||
|
||||
teammembershipService, err := teammembership.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
75
api/bolt/deploykey/deploykey.go
Normal file
75
api/bolt/deploykey/deploykey.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package deploykey
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "deploykeys"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Keys return an array containing all the keys.
|
||||
func (service *Service) Deploykeys() ([]portainer.Deploykey, error) {
|
||||
var deploykeys = make([]portainer.Deploykey, 0)
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var deploykey portainer.Deploykey
|
||||
err := internal.UnmarshalObject(v, &deploykey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deploykeys = append(deploykeys, deploykey)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return deploykeys, err
|
||||
}
|
||||
|
||||
// CreateKey creates a new key.
|
||||
func (service *Service) CreateDeploykey(deploykey *portainer.Deploykey) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
deploykey.ID = portainer.DeploykeyID(id)
|
||||
|
||||
data, err := internal.MarshalObject(deploykey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(deploykey.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteDeploykey deletes a key.
|
||||
func (service *Service) DeleteDeploykey(ID portainer.DeploykeyID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
||||
@@ -94,6 +94,11 @@ func initCryptoService() portainer.CryptoService {
|
||||
return &crypto.Service{}
|
||||
}
|
||||
|
||||
func initDigitalDeploykeyService() portainer.DigitalDeploykeyService {
|
||||
return &crypto.ECDSAService{}
|
||||
}
|
||||
|
||||
|
||||
func initLDAPService() portainer.LDAPService {
|
||||
return &ldap.Service{}
|
||||
}
|
||||
@@ -401,6 +406,8 @@ func main() {
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
digitalDeploykeyService := initDigitalDeploykeyService()
|
||||
|
||||
err := initKeyPair(fileService, digitalSignatureService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -498,6 +505,8 @@ func main() {
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
DeploykeyService: store.DeploykeyService,
|
||||
DigitalDeploykeyService: digitalDeploykeyService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
RegistryService: store.RegistryService,
|
||||
|
||||
378
api/crypto/certs.go
Normal file
378
api/crypto/certs.go
Normal file
@@ -0,0 +1,378 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// These constants from [PROTOCOL.certkeys] represent the algorithm names
|
||||
// for certificate types supported by this package.
|
||||
const (
|
||||
CertAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com"
|
||||
CertAlgoDSAv01 = "ssh-dss-cert-v01@openssh.com"
|
||||
CertAlgoECDSA256v01 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
|
||||
CertAlgoECDSA384v01 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
|
||||
CertAlgoECDSA521v01 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
|
||||
)
|
||||
|
||||
// Certificate types are used to specify whether a certificate is for identification
|
||||
// of a user or a host. Current identities are defined in [PROTOCOL.certkeys].
|
||||
const (
|
||||
UserCert = 1
|
||||
HostCert = 2
|
||||
)
|
||||
|
||||
type signature struct {
|
||||
Format string
|
||||
Blob []byte
|
||||
}
|
||||
|
||||
type tuple struct {
|
||||
Name string
|
||||
Data string
|
||||
}
|
||||
|
||||
const (
|
||||
maxUint64 = 1<<64 - 1
|
||||
maxInt64 = 1<<63 - 1
|
||||
)
|
||||
|
||||
// CertTime represents an unsigned 64-bit time value in seconds starting from
|
||||
// UNIX epoch. We use CertTime instead of time.Time in order to properly handle
|
||||
// the "infinite" time value ^0, which would become negative when expressed as
|
||||
// an int64.
|
||||
type CertTime uint64
|
||||
|
||||
func (ct CertTime) Time() time.Time {
|
||||
if ct > maxInt64 {
|
||||
return time.Unix(maxInt64, 0)
|
||||
}
|
||||
return time.Unix(int64(ct), 0)
|
||||
}
|
||||
|
||||
func (ct CertTime) IsInfinite() bool {
|
||||
return ct == maxUint64
|
||||
}
|
||||
|
||||
// An OpenSSHCertV01 represents an OpenSSH certificate as defined in
|
||||
// [PROTOCOL.certkeys]?rev=1.8.
|
||||
type OpenSSHCertV01 struct {
|
||||
Nonce []byte
|
||||
Key PublicKey
|
||||
Serial uint64
|
||||
Type uint32
|
||||
KeyId string
|
||||
ValidPrincipals []string
|
||||
ValidAfter, ValidBefore CertTime
|
||||
CriticalOptions []tuple
|
||||
Extensions []tuple
|
||||
Reserved []byte
|
||||
SignatureKey PublicKey
|
||||
Signature *signature
|
||||
}
|
||||
|
||||
// validateOpenSSHCertV01Signature uses the cert's SignatureKey to verify that
|
||||
// the cert's Signature.Blob is the result of signing the cert bytes starting
|
||||
// from the algorithm string and going up to and including the SignatureKey.
|
||||
func validateOpenSSHCertV01Signature(cert *OpenSSHCertV01) bool {
|
||||
return cert.SignatureKey.Verify(cert.BytesForSigning(), cert.Signature.Blob)
|
||||
}
|
||||
|
||||
var certAlgoNames = map[string]string{
|
||||
KeyAlgoRSA: CertAlgoRSAv01,
|
||||
KeyAlgoDSA: CertAlgoDSAv01,
|
||||
KeyAlgoECDSA256: CertAlgoECDSA256v01,
|
||||
KeyAlgoECDSA384: CertAlgoECDSA384v01,
|
||||
KeyAlgoECDSA521: CertAlgoECDSA521v01,
|
||||
}
|
||||
|
||||
// certToPrivAlgo returns the underlying algorithm for a certificate algorithm.
|
||||
// Panics if a non-certificate algorithm is passed.
|
||||
func certToPrivAlgo(algo string) string {
|
||||
for privAlgo, pubAlgo := range certAlgoNames {
|
||||
if pubAlgo == algo {
|
||||
return privAlgo
|
||||
}
|
||||
}
|
||||
panic("unknown cert algorithm")
|
||||
}
|
||||
|
||||
func (cert *OpenSSHCertV01) marshal(includeAlgo, includeSig bool) []byte {
|
||||
algoName := cert.PublicKeyAlgo()
|
||||
pubKey := cert.Key.Marshal()
|
||||
sigKey := MarshalPublicKey(cert.SignatureKey)
|
||||
|
||||
var length int
|
||||
if includeAlgo {
|
||||
length += stringLength(len(algoName))
|
||||
}
|
||||
length += stringLength(len(cert.Nonce))
|
||||
length += len(pubKey)
|
||||
length += 8 // Length of Serial
|
||||
length += 4 // Length of Type
|
||||
length += stringLength(len(cert.KeyId))
|
||||
length += lengthPrefixedNameListLength(cert.ValidPrincipals)
|
||||
length += 8 // Length of ValidAfter
|
||||
length += 8 // Length of ValidBefore
|
||||
length += tupleListLength(cert.CriticalOptions)
|
||||
length += tupleListLength(cert.Extensions)
|
||||
length += stringLength(len(cert.Reserved))
|
||||
length += stringLength(len(sigKey))
|
||||
if includeSig {
|
||||
length += signatureLength(cert.Signature)
|
||||
}
|
||||
|
||||
ret := make([]byte, length)
|
||||
r := ret
|
||||
if includeAlgo {
|
||||
r = marshalString(r, []byte(algoName))
|
||||
}
|
||||
r = marshalString(r, cert.Nonce)
|
||||
copy(r, pubKey)
|
||||
r = r[len(pubKey):]
|
||||
r = marshalUint64(r, cert.Serial)
|
||||
r = marshalUint32(r, cert.Type)
|
||||
r = marshalString(r, []byte(cert.KeyId))
|
||||
r = marshalLengthPrefixedNameList(r, cert.ValidPrincipals)
|
||||
r = marshalUint64(r, uint64(cert.ValidAfter))
|
||||
r = marshalUint64(r, uint64(cert.ValidBefore))
|
||||
r = marshalTupleList(r, cert.CriticalOptions)
|
||||
r = marshalTupleList(r, cert.Extensions)
|
||||
r = marshalString(r, cert.Reserved)
|
||||
r = marshalString(r, sigKey)
|
||||
if includeSig {
|
||||
r = marshalSignature(r, cert.Signature)
|
||||
}
|
||||
if len(r) > 0 {
|
||||
panic("ssh: internal error, marshaling certificate did not fill the entire buffer")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (cert *OpenSSHCertV01) BytesForSigning() []byte {
|
||||
return cert.marshal(true, false)
|
||||
}
|
||||
|
||||
func (cert *OpenSSHCertV01) Marshal() []byte {
|
||||
return cert.marshal(false, true)
|
||||
}
|
||||
|
||||
func (c *OpenSSHCertV01) PublicKeyAlgo() string {
|
||||
algo, ok := certAlgoNames[c.Key.PublicKeyAlgo()]
|
||||
if !ok {
|
||||
panic("unknown cert key type")
|
||||
}
|
||||
return algo
|
||||
}
|
||||
|
||||
func (c *OpenSSHCertV01) PrivateKeyAlgo() string {
|
||||
return c.Key.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
func (c *OpenSSHCertV01) Verify(data []byte, sig []byte) bool {
|
||||
return c.Key.Verify(data, sig)
|
||||
}
|
||||
|
||||
func parseOpenSSHCertV01(in []byte, algo string) (out *OpenSSHCertV01, rest []byte, ok bool) {
|
||||
cert := new(OpenSSHCertV01)
|
||||
|
||||
if cert.Nonce, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
privAlgo := certToPrivAlgo(algo)
|
||||
cert.Key, in, ok = parsePubKey(in, privAlgo)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// We test PublicKeyAlgo to make sure we don't use some weird sub-cert.
|
||||
if cert.Key.PublicKeyAlgo() != privAlgo {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Serial, in, ok = parseUint64(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Type, in, ok = parseUint32(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
keyId, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cert.KeyId = string(keyId)
|
||||
|
||||
if cert.ValidPrincipals, in, ok = parseLengthPrefixedNameList(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
va, in, ok := parseUint64(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cert.ValidAfter = CertTime(va)
|
||||
|
||||
vb, in, ok := parseUint64(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cert.ValidBefore = CertTime(vb)
|
||||
|
||||
if cert.CriticalOptions, in, ok = parseTupleList(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Extensions, in, ok = parseTupleList(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Reserved, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
sigKey, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if cert.SignatureKey, _, ok = ParsePublicKey(sigKey); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Signature, in, ok = parseSignature(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return cert, in, ok
|
||||
}
|
||||
|
||||
func lengthPrefixedNameListLength(namelist []string) int {
|
||||
length := 4 // length prefix for list
|
||||
for _, name := range namelist {
|
||||
length += 4 // length prefix for name
|
||||
length += len(name)
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalLengthPrefixedNameList(to []byte, namelist []string) []byte {
|
||||
length := uint32(lengthPrefixedNameListLength(namelist) - 4)
|
||||
to = marshalUint32(to, length)
|
||||
for _, name := range namelist {
|
||||
to = marshalString(to, []byte(name))
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
func parseLengthPrefixedNameList(in []byte) (out []string, rest []byte, ok bool) {
|
||||
list, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for len(list) > 0 {
|
||||
var next []byte
|
||||
if next, list, ok = parseString(list); !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
out = append(out, string(next))
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func tupleListLength(tupleList []tuple) int {
|
||||
length := 4 // length prefix for list
|
||||
for _, t := range tupleList {
|
||||
length += 4 // length prefix for t.Name
|
||||
length += len(t.Name)
|
||||
length += 4 // length prefix for t.Data
|
||||
length += len(t.Data)
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalTupleList(to []byte, tuplelist []tuple) []byte {
|
||||
length := uint32(tupleListLength(tuplelist) - 4)
|
||||
to = marshalUint32(to, length)
|
||||
for _, t := range tuplelist {
|
||||
to = marshalString(to, []byte(t.Name))
|
||||
to = marshalString(to, []byte(t.Data))
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
func parseTupleList(in []byte) (out []tuple, rest []byte, ok bool) {
|
||||
list, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for len(list) > 0 {
|
||||
var name, data []byte
|
||||
var ok bool
|
||||
name, list, ok = parseString(list)
|
||||
if !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
data, list, ok = parseString(list)
|
||||
if !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
out = append(out, tuple{string(name), string(data)})
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func signatureLength(sig *signature) int {
|
||||
length := 4 // length prefix for signature
|
||||
length += stringLength(len(sig.Format))
|
||||
length += stringLength(len(sig.Blob))
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalSignature(to []byte, sig *signature) []byte {
|
||||
length := uint32(signatureLength(sig) - 4)
|
||||
to = marshalUint32(to, length)
|
||||
to = marshalString(to, []byte(sig.Format))
|
||||
to = marshalString(to, sig.Blob)
|
||||
return to
|
||||
}
|
||||
|
||||
func parseSignatureBody(in []byte) (out *signature, rest []byte, ok bool) {
|
||||
var format []byte
|
||||
if format, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
out = &signature{
|
||||
Format: string(format),
|
||||
}
|
||||
|
||||
if out.Blob, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
return out, in, ok
|
||||
}
|
||||
|
||||
func parseSignature(in []byte) (out *signature, rest []byte, ok bool) {
|
||||
var sigBytes []byte
|
||||
if sigBytes, rest, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
out, sigBytes, ok = parseSignatureBody(sigBytes)
|
||||
if !ok || len(sigBytes) > 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package crypto
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -91,6 +93,29 @@ func (service *ECDSAService) GenerateKeyPair() ([]byte, []byte, error) {
|
||||
return private, public, nil
|
||||
}
|
||||
|
||||
// GenerateKeyPair will create a new key pair using ECDSA.
|
||||
func (service *ECDSAService) GenerateSshKey() ([]byte, error) {
|
||||
//savePublicFileTo := "./id_ecdsa_test.pub"
|
||||
|
||||
pubkeyCurve := elliptic.P256()
|
||||
|
||||
byteKeys, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
|
||||
publicKeyBytes, err := generateECDSAPublicKey(&byteKeys.PublicKey)
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
log.Println(publicKeyBytes)
|
||||
err = writeKeyToFile(publicKeyBytes,"./testFile" )
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
return publicKeyBytes, nil
|
||||
}
|
||||
|
||||
// Sign creates a signature from a message.
|
||||
// It automatically hash the message using MD5 and creates a signature from
|
||||
// that hash.
|
||||
@@ -120,3 +145,22 @@ func (service *ECDSAService) Sign(message string) (string, error) {
|
||||
|
||||
return base64.RawStdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
func generateECDSAPublicKey(privatekey *ecdsa.PublicKey) ([]byte, error) {
|
||||
publicRsaKey, err := NewPublicKey(privatekey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pubKeyBytes := MarshalAuthorizedKey(publicRsaKey)
|
||||
return pubKeyBytes, nil
|
||||
}
|
||||
// writePemToFile writes keys to a file
|
||||
func writeKeyToFile(keyBytes []byte, saveFileTo string) error {
|
||||
log.Println("hey")
|
||||
err := ioutil.WriteFile(saveFileTo, keyBytes, 0777)
|
||||
log.Println("here")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
615
api/crypto/keys.go
Normal file
615
api/crypto/keys.go
Normal file
@@ -0,0 +1,615 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This package is a duplicate of 32844aa1ae54: https://code.google.com/p/go/source/browse/ssh/keys.go?repo=crypto
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// These constants represent the algorithm names for key types supported by this
|
||||
// package.
|
||||
const (
|
||||
KeyAlgoRSA = "ssh-rsa"
|
||||
KeyAlgoDSA = "ssh-dss"
|
||||
KeyAlgoECDSA256 = "ecdsa-sha2-nistp256"
|
||||
KeyAlgoECDSA384 = "ecdsa-sha2-nistp384"
|
||||
KeyAlgoECDSA521 = "ecdsa-sha2-nistp521"
|
||||
)
|
||||
|
||||
// parsePubKey parses a public key of the given algorithm.
|
||||
// Use ParsePublicKey for keys with prepended algorithm.
|
||||
func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, ok bool) {
|
||||
|
||||
switch algo {
|
||||
case KeyAlgoRSA:
|
||||
return parseRSA(in)
|
||||
case KeyAlgoDSA:
|
||||
return parseDSA(in)
|
||||
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
|
||||
return parseECDSA(in)
|
||||
case CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01:
|
||||
return parseOpenSSHCertV01(in, algo)
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
|
||||
|
||||
// parseAuthorizedKey parses a public key in OpenSSH authorized_keys format
|
||||
// (see sshd(8) manual page) once the options and key type fields have been
|
||||
// removed.
|
||||
func parseAuthorizedKey(in []byte) (out PublicKey, comment string, ok bool) {
|
||||
in = bytes.TrimSpace(in)
|
||||
|
||||
i := bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
i = len(in)
|
||||
}
|
||||
base64Key := in[:i]
|
||||
|
||||
key := make([]byte, base64.StdEncoding.DecodedLen(len(base64Key)))
|
||||
n, err := base64.StdEncoding.Decode(key, base64Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key = key[:n]
|
||||
out, _, ok = ParsePublicKey(key)
|
||||
if !ok {
|
||||
return nil, "", false
|
||||
}
|
||||
comment = string(bytes.TrimSpace(in[i:]))
|
||||
return
|
||||
}
|
||||
|
||||
// ParseAuthorizedKeys parses a public key from an authorized_keys
|
||||
// file used in OpenSSH according to the sshd(8) manual page.
|
||||
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, ok bool) {
|
||||
for len(in) > 0 {
|
||||
end := bytes.IndexByte(in, '\n')
|
||||
if end != -1 {
|
||||
rest = in[end+1:]
|
||||
in = in[:end]
|
||||
} else {
|
||||
rest = nil
|
||||
}
|
||||
|
||||
end = bytes.IndexByte(in, '\r')
|
||||
if end != -1 {
|
||||
in = in[:end]
|
||||
}
|
||||
|
||||
in = bytes.TrimSpace(in)
|
||||
if len(in) == 0 || in[0] == '#' {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
i := bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
if out, comment, ok = parseAuthorizedKey(in[i:]); ok {
|
||||
return
|
||||
}
|
||||
|
||||
// No key type recognised. Maybe there's an options field at
|
||||
// the beginning.
|
||||
var b byte
|
||||
inQuote := false
|
||||
var candidateOptions []string
|
||||
optionStart := 0
|
||||
for i, b = range in {
|
||||
isEnd := !inQuote && (b == ' ' || b == '\t')
|
||||
if (b == ',' && !inQuote) || isEnd {
|
||||
if i-optionStart > 0 {
|
||||
candidateOptions = append(candidateOptions, string(in[optionStart:i]))
|
||||
}
|
||||
optionStart = i + 1
|
||||
}
|
||||
if isEnd {
|
||||
break
|
||||
}
|
||||
if b == '"' && (i == 0 || (i > 0 && in[i-1] != '\\')) {
|
||||
inQuote = !inQuote
|
||||
}
|
||||
}
|
||||
for i < len(in) && (in[i] == ' ' || in[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i == len(in) {
|
||||
// Invalid line: unmatched quote
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
in = in[i:]
|
||||
i = bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
if out, comment, ok = parseAuthorizedKey(in[i:]); ok {
|
||||
options = candidateOptions
|
||||
return
|
||||
}
|
||||
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ParsePublicKey parses an SSH public key formatted for use in
|
||||
// the SSH wire protocol according to RFC 4253, section 6.6.
|
||||
func ParsePublicKey(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
algo, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
return parsePubKey(in, string(algo))
|
||||
}
|
||||
|
||||
// MarshalAuthorizedKey returns a byte stream suitable for inclusion
|
||||
// in an OpenSSH authorized_keys file following the format specified
|
||||
// in the sshd(8) manual page.
|
||||
func MarshalAuthorizedKey(key PublicKey) []byte {
|
||||
b := &bytes.Buffer{}
|
||||
b.WriteString(key.PublicKeyAlgo())
|
||||
b.WriteByte(' ')
|
||||
e := base64.NewEncoder(base64.StdEncoding, b)
|
||||
e.Write(MarshalPublicKey(key))
|
||||
e.Close()
|
||||
b.WriteByte('\n')
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// PublicKey is an abstraction of different types of public keys.
|
||||
type PublicKey interface {
|
||||
// PrivateKeyAlgo returns the name of the encryption system.
|
||||
PrivateKeyAlgo() string
|
||||
|
||||
// PublicKeyAlgo returns the algorithm for the public key,
|
||||
// which may be different from PrivateKeyAlgo for certificates.
|
||||
PublicKeyAlgo() string
|
||||
|
||||
// Marshal returns the serialized key data in SSH wire format,
|
||||
// without the name prefix. Callers should typically use
|
||||
// MarshalPublicKey().
|
||||
Marshal() []byte
|
||||
|
||||
// Verify that sig is a signature on the given data using this
|
||||
// key. This function will hash the data appropriately first.
|
||||
Verify(data []byte, sigBlob []byte) bool
|
||||
}
|
||||
|
||||
// A Signer is can create signatures that verify against a public key.
|
||||
type Signer interface {
|
||||
// PublicKey returns an associated PublicKey instance.
|
||||
PublicKey() PublicKey
|
||||
|
||||
// Sign returns raw signature for the given data. This method
|
||||
// will apply the hash specified for the keytype to the data.
|
||||
Sign(rand io.Reader, data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type rsaPublicKey rsa.PublicKey
|
||||
|
||||
func (r *rsaPublicKey) PrivateKeyAlgo() string {
|
||||
return "ssh-rsa"
|
||||
}
|
||||
|
||||
func (r *rsaPublicKey) PublicKeyAlgo() string {
|
||||
return r.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
// parseRSA parses an RSA key according to RFC 4253, section 6.6.
|
||||
func parseRSA(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
key := new(rsa.PublicKey)
|
||||
|
||||
bigE, in, ok := parseInt(in)
|
||||
if !ok || bigE.BitLen() > 24 {
|
||||
return
|
||||
}
|
||||
e := bigE.Int64()
|
||||
if e < 3 || e&1 == 0 {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
key.E = int(e)
|
||||
|
||||
if key.N, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return (*rsaPublicKey)(key), in, ok
|
||||
}
|
||||
|
||||
func (r *rsaPublicKey) Marshal() []byte {
|
||||
// See RFC 4253, section 6.6.
|
||||
e := new(big.Int).SetInt64(int64(r.E))
|
||||
length := intLength(e)
|
||||
length += intLength(r.N)
|
||||
|
||||
ret := make([]byte, length)
|
||||
rest := marshalInt(ret, e)
|
||||
marshalInt(rest, r.N)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *rsaPublicKey) Verify(data []byte, sig []byte) bool {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
return rsa.VerifyPKCS1v15((*rsa.PublicKey)(r), crypto.SHA1, digest, sig) == nil
|
||||
}
|
||||
|
||||
type rsaPrivateKey struct {
|
||||
*rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (r *rsaPrivateKey) PublicKey() PublicKey {
|
||||
return (*rsaPublicKey)(&r.PrivateKey.PublicKey)
|
||||
}
|
||||
|
||||
func (r *rsaPrivateKey) Sign(rand io.Reader, data []byte) ([]byte, error) {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
return rsa.SignPKCS1v15(rand, r.PrivateKey, crypto.SHA1, digest)
|
||||
}
|
||||
|
||||
type dsaPublicKey dsa.PublicKey
|
||||
|
||||
func (r *dsaPublicKey) PrivateKeyAlgo() string {
|
||||
return "ssh-dss"
|
||||
}
|
||||
|
||||
func (r *dsaPublicKey) PublicKeyAlgo() string {
|
||||
return r.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
// parseDSA parses an DSA key according to RFC 4253, section 6.6.
|
||||
func parseDSA(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
key := new(dsa.PublicKey)
|
||||
|
||||
if key.P, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if key.Q, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if key.G, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if key.Y, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return (*dsaPublicKey)(key), in, ok
|
||||
}
|
||||
|
||||
func (r *dsaPublicKey) Marshal() []byte {
|
||||
// See RFC 4253, section 6.6.
|
||||
length := intLength(r.P)
|
||||
length += intLength(r.Q)
|
||||
length += intLength(r.G)
|
||||
length += intLength(r.Y)
|
||||
|
||||
ret := make([]byte, length)
|
||||
rest := marshalInt(ret, r.P)
|
||||
rest = marshalInt(rest, r.Q)
|
||||
rest = marshalInt(rest, r.G)
|
||||
marshalInt(rest, r.Y)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (k *dsaPublicKey) Verify(data []byte, sigBlob []byte) bool {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
|
||||
// Per RFC 4253, section 6.6,
|
||||
// The value for 'dss_signature_blob' is encoded as a string containing
|
||||
// r, followed by s (which are 160-bit integers, without lengths or
|
||||
// padding, unsigned, and in network byte order).
|
||||
// For DSS purposes, sig.Blob should be exactly 40 bytes in length.
|
||||
if len(sigBlob) != 40 {
|
||||
return false
|
||||
}
|
||||
r := new(big.Int).SetBytes(sigBlob[:20])
|
||||
s := new(big.Int).SetBytes(sigBlob[20:])
|
||||
return dsa.Verify((*dsa.PublicKey)(k), digest, r, s)
|
||||
}
|
||||
|
||||
type dsaPrivateKey struct {
|
||||
*dsa.PrivateKey
|
||||
}
|
||||
|
||||
func (k *dsaPrivateKey) PublicKey() PublicKey {
|
||||
return (*dsaPublicKey)(&k.PrivateKey.PublicKey)
|
||||
}
|
||||
|
||||
func (k *dsaPrivateKey) Sign(rand io.Reader, data []byte) ([]byte, error) {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
r, s, err := dsa.Sign(rand, k.PrivateKey, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig := make([]byte, 40)
|
||||
copy(sig[:20], r.Bytes())
|
||||
copy(sig[20:], s.Bytes())
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
type ecdsaPublicKey ecdsa.PublicKey
|
||||
|
||||
func (key *ecdsaPublicKey) PrivateKeyAlgo() string {
|
||||
return "ecdsa-sha2-" + key.nistID()
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) nistID() string {
|
||||
switch key.Params().BitSize {
|
||||
case 256:
|
||||
return "nistp256"
|
||||
case 384:
|
||||
return "nistp384"
|
||||
case 521:
|
||||
return "nistp521"
|
||||
}
|
||||
panic("ssh: unsupported ecdsa key size")
|
||||
}
|
||||
|
||||
func supportedEllipticCurve(curve elliptic.Curve) bool {
|
||||
return (curve == elliptic.P256() || curve == elliptic.P384() || curve == elliptic.P521())
|
||||
}
|
||||
|
||||
// ecHash returns the hash to match the given elliptic curve, see RFC
|
||||
// 5656, section 6.2.1
|
||||
func ecHash(curve elliptic.Curve) crypto.Hash {
|
||||
bitSize := curve.Params().BitSize
|
||||
switch {
|
||||
case bitSize <= 256:
|
||||
return crypto.SHA256
|
||||
case bitSize <= 384:
|
||||
return crypto.SHA384
|
||||
}
|
||||
return crypto.SHA512
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) PublicKeyAlgo() string {
|
||||
return key.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
|
||||
func parseECDSA(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
var identifier []byte
|
||||
if identifier, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
key := new(ecdsa.PublicKey)
|
||||
|
||||
switch string(identifier) {
|
||||
case "nistp256":
|
||||
key.Curve = elliptic.P256()
|
||||
case "nistp384":
|
||||
key.Curve = elliptic.P384()
|
||||
case "nistp521":
|
||||
key.Curve = elliptic.P521()
|
||||
default:
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
var keyBytes []byte
|
||||
if keyBytes, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
key.X, key.Y = elliptic.Unmarshal(key.Curve, keyBytes)
|
||||
if key.X == nil || key.Y == nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
return (*ecdsaPublicKey)(key), in, ok
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) Marshal() []byte {
|
||||
// See RFC 5656, section 3.1.
|
||||
keyBytes := elliptic.Marshal(key.Curve, key.X, key.Y)
|
||||
|
||||
ID := key.nistID()
|
||||
length := stringLength(len(ID))
|
||||
length += stringLength(len(keyBytes))
|
||||
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(ID))
|
||||
r = marshalString(r, keyBytes)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) Verify(data []byte, sigBlob []byte) bool {
|
||||
h := ecHash(key.Curve).New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
|
||||
// Per RFC 5656, section 3.1.2,
|
||||
// The ecdsa_signature_blob value has the following specific encoding:
|
||||
// mpint r
|
||||
// mpint s
|
||||
r, rest, ok := parseInt(sigBlob)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
s, rest, ok := parseInt(rest)
|
||||
if !ok || len(rest) > 0 {
|
||||
return false
|
||||
}
|
||||
return ecdsa.Verify((*ecdsa.PublicKey)(key), digest, r, s)
|
||||
}
|
||||
|
||||
type ecdsaPrivateKey struct {
|
||||
*ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
func (k *ecdsaPrivateKey) PublicKey() PublicKey {
|
||||
return (*ecdsaPublicKey)(&k.PrivateKey.PublicKey)
|
||||
}
|
||||
|
||||
func (k *ecdsaPrivateKey) Sign(rand io.Reader, data []byte) ([]byte, error) {
|
||||
h := ecHash(k.PrivateKey.PublicKey.Curve).New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
r, s, err := ecdsa.Sign(rand, k.PrivateKey, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig := make([]byte, intLength(r)+intLength(s))
|
||||
rest := marshalInt(sig, r)
|
||||
marshalInt(rest, s)
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// NewPrivateKey takes a pointer to rsa, dsa or ecdsa PrivateKey
|
||||
// returns a corresponding Signer instance. EC keys should use P256,
|
||||
// P384 or P521.
|
||||
func NewSignerFromKey(k interface{}) (Signer, error) {
|
||||
var sshKey Signer
|
||||
switch t := k.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
sshKey = &rsaPrivateKey{t}
|
||||
case *dsa.PrivateKey:
|
||||
sshKey = &dsaPrivateKey{t}
|
||||
case *ecdsa.PrivateKey:
|
||||
if !supportedEllipticCurve(t.Curve) {
|
||||
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
|
||||
}
|
||||
|
||||
sshKey = &ecdsaPrivateKey{t}
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
|
||||
}
|
||||
return sshKey, nil
|
||||
}
|
||||
|
||||
// NewPublicKey takes a pointer to rsa, dsa or ecdsa PublicKey
|
||||
// and returns a corresponding ssh PublicKey instance. EC keys should use P256, P384 or P521.
|
||||
func NewPublicKey(k interface{}) (PublicKey, error) {
|
||||
|
||||
var sshKey PublicKey
|
||||
|
||||
switch t := k.(type) {
|
||||
case *rsa.PublicKey:
|
||||
sshKey = (*rsaPublicKey)(t)
|
||||
case *ecdsa.PublicKey:
|
||||
if !supportedEllipticCurve(t.Curve) {
|
||||
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
|
||||
}
|
||||
sshKey = (*ecdsaPublicKey)(t)
|
||||
case *dsa.PublicKey:
|
||||
sshKey = (*dsaPublicKey)(t)
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
|
||||
}
|
||||
return sshKey, nil
|
||||
}
|
||||
|
||||
// ParsePublicKey parses a PEM encoded private key. It supports
|
||||
// PKCS#1, RSA, DSA and ECDSA private keys.
|
||||
func ParsePrivateKey(pemBytes []byte) (Signer, error) {
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
return nil, errors.New("ssh: no key found")
|
||||
}
|
||||
|
||||
var rawkey interface{}
|
||||
switch block.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
rsa, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawkey = rsa
|
||||
case "EC PRIVATE KEY":
|
||||
ec, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawkey = ec
|
||||
case "DSA PRIVATE KEY":
|
||||
ec, err := parseDSAPrivate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawkey = ec
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
|
||||
}
|
||||
|
||||
return NewSignerFromKey(rawkey)
|
||||
}
|
||||
|
||||
// parseDSAPrivate parses a DSA key in ASN.1 DER encoding, as
|
||||
// documented in the OpenSSL DSA manpage.
|
||||
// TODO(hanwen): move this in to crypto/x509 after the Go 1.2 freeze.
|
||||
func parseDSAPrivate(p []byte) (*dsa.PrivateKey, error) {
|
||||
k := struct {
|
||||
Version int
|
||||
P *big.Int
|
||||
Q *big.Int
|
||||
G *big.Int
|
||||
Priv *big.Int
|
||||
Pub *big.Int
|
||||
}{}
|
||||
rest, err := asn1.Unmarshal(p, &k)
|
||||
if err != nil {
|
||||
return nil, errors.New("ssh: failed to parse DSA key: " + err.Error())
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, errors.New("ssh: garbage after DSA key")
|
||||
}
|
||||
|
||||
return &dsa.PrivateKey{
|
||||
PublicKey: dsa.PublicKey{
|
||||
Parameters: dsa.Parameters{
|
||||
P: k.P,
|
||||
Q: k.Q,
|
||||
G: k.G,
|
||||
},
|
||||
Y: k.Priv,
|
||||
},
|
||||
X: k.Pub,
|
||||
}, nil
|
||||
}
|
||||
187
api/crypto/private.go
Normal file
187
api/crypto/private.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package crypto
|
||||
|
||||
// Copy only of used functions from message.go, common.go
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
kexAlgoDH1SHA1 = "diffie-hellman-group1-sha1"
|
||||
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
|
||||
kexAlgoECDH256 = "ecdh-sha2-nistp256"
|
||||
kexAlgoECDH384 = "ecdh-sha2-nistp384"
|
||||
kexAlgoECDH521 = "ecdh-sha2-nistp521"
|
||||
)
|
||||
|
||||
var bigOne = big.NewInt(1)
|
||||
|
||||
func stringLength(n int) int {
|
||||
return 4 + n
|
||||
}
|
||||
|
||||
// MarshalPublicKey serializes a supported key or certificate for use
|
||||
// by the SSH wire protocol. It can be used for comparison with the
|
||||
// pubkey argument of ServerConfig's PublicKeyCallback as well as for
|
||||
// generating an authorized_keys or host_keys file.
|
||||
func MarshalPublicKey(key PublicKey) []byte {
|
||||
// See also RFC 4253 6.6.
|
||||
algoname := key.PublicKeyAlgo()
|
||||
blob := key.Marshal()
|
||||
|
||||
length := stringLength(len(algoname))
|
||||
length += len(blob)
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(algoname))
|
||||
copy(r, blob)
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseInt(in []byte) (out *big.Int, rest []byte, ok bool) {
|
||||
contents, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
out = new(big.Int)
|
||||
|
||||
if len(contents) > 0 && contents[0]&0x80 == 0x80 {
|
||||
// This is a negative number
|
||||
notBytes := make([]byte, len(contents))
|
||||
for i := range notBytes {
|
||||
notBytes[i] = ^contents[i]
|
||||
}
|
||||
out.SetBytes(notBytes)
|
||||
out.Add(out, bigOne)
|
||||
out.Neg(out)
|
||||
} else {
|
||||
// Positive number
|
||||
out.SetBytes(contents)
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func parseUint32(in []byte) (uint32, []byte, bool) {
|
||||
if len(in) < 4 {
|
||||
return 0, nil, false
|
||||
}
|
||||
return binary.BigEndian.Uint32(in), in[4:], true
|
||||
}
|
||||
|
||||
func parseUint64(in []byte) (uint64, []byte, bool) {
|
||||
if len(in) < 8 {
|
||||
return 0, nil, false
|
||||
}
|
||||
return binary.BigEndian.Uint64(in), in[8:], true
|
||||
}
|
||||
|
||||
func parseString(in []byte) (out, rest []byte, ok bool) {
|
||||
if len(in) < 4 {
|
||||
return
|
||||
}
|
||||
length := binary.BigEndian.Uint32(in)
|
||||
if uint32(len(in)) < 4+length {
|
||||
return
|
||||
}
|
||||
out = in[4 : 4+length]
|
||||
rest = in[4+length:]
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func intLength(n *big.Int) int {
|
||||
length := 4 /* length bytes */
|
||||
if n.Sign() < 0 {
|
||||
nMinus1 := new(big.Int).Neg(n)
|
||||
nMinus1.Sub(nMinus1, bigOne)
|
||||
bitLen := nMinus1.BitLen()
|
||||
if bitLen%8 == 0 {
|
||||
// The number will need 0xff padding
|
||||
length++
|
||||
}
|
||||
length += (bitLen + 7) / 8
|
||||
} else if n.Sign() == 0 {
|
||||
// A zero is the zero length string
|
||||
} else {
|
||||
bitLen := n.BitLen()
|
||||
if bitLen%8 == 0 {
|
||||
// The number will need 0x00 padding
|
||||
length++
|
||||
}
|
||||
length += (bitLen + 7) / 8
|
||||
}
|
||||
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalUint32(to []byte, n uint32) []byte {
|
||||
binary.BigEndian.PutUint32(to, n)
|
||||
return to[4:]
|
||||
}
|
||||
|
||||
func marshalUint64(to []byte, n uint64) []byte {
|
||||
binary.BigEndian.PutUint64(to, n)
|
||||
return to[8:]
|
||||
}
|
||||
|
||||
func marshalInt(to []byte, n *big.Int) []byte {
|
||||
lengthBytes := to
|
||||
to = to[4:]
|
||||
length := 0
|
||||
|
||||
if n.Sign() < 0 {
|
||||
// A negative number has to be converted to two's-complement
|
||||
// form. So we'll subtract 1 and invert. If the
|
||||
// most-significant-bit isn't set then we'll need to pad the
|
||||
// beginning with 0xff in order to keep the number negative.
|
||||
nMinus1 := new(big.Int).Neg(n)
|
||||
nMinus1.Sub(nMinus1, bigOne)
|
||||
bytes := nMinus1.Bytes()
|
||||
for i := range bytes {
|
||||
bytes[i] ^= 0xff
|
||||
}
|
||||
if len(bytes) == 0 || bytes[0]&0x80 == 0 {
|
||||
to[0] = 0xff
|
||||
to = to[1:]
|
||||
length++
|
||||
}
|
||||
nBytes := copy(to, bytes)
|
||||
to = to[nBytes:]
|
||||
length += nBytes
|
||||
} else if n.Sign() == 0 {
|
||||
// A zero is the zero length string
|
||||
} else {
|
||||
bytes := n.Bytes()
|
||||
if len(bytes) > 0 && bytes[0]&0x80 != 0 {
|
||||
// We'll have to pad this with a 0x00 in order to
|
||||
// stop it looking like a negative number.
|
||||
to[0] = 0
|
||||
to = to[1:]
|
||||
length++
|
||||
}
|
||||
nBytes := copy(to, bytes)
|
||||
to = to[nBytes:]
|
||||
length += nBytes
|
||||
}
|
||||
|
||||
lengthBytes[0] = byte(length >> 24)
|
||||
lengthBytes[1] = byte(length >> 16)
|
||||
lengthBytes[2] = byte(length >> 8)
|
||||
lengthBytes[3] = byte(length)
|
||||
return to
|
||||
}
|
||||
|
||||
func marshalString(to []byte, s []byte) []byte {
|
||||
to[0] = byte(len(s) >> 24)
|
||||
to[1] = byte(len(s) >> 16)
|
||||
to[2] = byte(len(s) >> 8)
|
||||
to[3] = byte(len(s))
|
||||
to = to[4:]
|
||||
copy(to, s)
|
||||
return to[len(s):]
|
||||
}
|
||||
@@ -71,6 +71,11 @@ const (
|
||||
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
|
||||
)
|
||||
|
||||
// Deploykey errors
|
||||
const (
|
||||
ErrDeploykeyAlreadyExists = Error("A key already exists with this name")
|
||||
)
|
||||
|
||||
// Crypto errors.
|
||||
const (
|
||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||
|
||||
76
api/http/handler/deploykeys/deploykey_create.go
Normal file
76
api/http/handler/deploykeys/deploykey_create.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type deploykeyCreatePayload struct {
|
||||
Name string
|
||||
Privatekeypath string
|
||||
Publickeypath string
|
||||
UserID int
|
||||
LastUsage string
|
||||
}
|
||||
|
||||
func (payload *deploykeyCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid deploykey Name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/deploykeys
|
||||
func (handler *Handler) deploykeyCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload deploykeyCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
deploykeys, err := handler.DeploykeyService.Deploykeys()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve deploykeys from the database", err}
|
||||
}
|
||||
|
||||
for _, deploykey := range deploykeys {
|
||||
if deploykey.Name == payload.Name {
|
||||
return &httperror.HandlerError{http.StatusConflict, "This name is already associated to a deploykey", portainer.ErrDeploykeyAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
pubkeypath, errrrr := handler.DigitalDeploykeyService.GenerateSshKey()
|
||||
|
||||
if errrrr != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid key payload", errrrr}
|
||||
}
|
||||
|
||||
//encodedStr := hex.EncodeToString(privatepath)
|
||||
encodedStr1 := hex.EncodeToString(pubkeypath)
|
||||
dateTime := time.Now().Local().Format("2006-01-02 15:04:05")
|
||||
|
||||
deploykey := &portainer.Deploykey{
|
||||
Name: payload.Name,
|
||||
Privatekeypath: "abc",
|
||||
Publickeypath: encodedStr1,
|
||||
UserID: payload.UserID,
|
||||
LastUsage: dateTime,
|
||||
}
|
||||
|
||||
err = handler.DeploykeyService.CreateDeploykey(deploykey)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the deploykey inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, deploykey)
|
||||
}
|
||||
func BytesToString(data []byte) string {
|
||||
return string(data[:])
|
||||
}
|
||||
25
api/http/handler/deploykeys/deploykey_delete.go
Normal file
25
api/http/handler/deploykeys/deploykey_delete.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// DELETE request on /api/deploykeys/:id
|
||||
func (handler *Handler) deploykeyDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid deploykey identifier route variable", err}
|
||||
}
|
||||
|
||||
err = handler.DeploykeyService.DeleteDeploykey(portainer.DeploykeyID(id))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the deploykey from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
18
api/http/handler/deploykeys/deploykey_list.go
Normal file
18
api/http/handler/deploykeys/deploykey_list.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// GET request on /api/deploykeys
|
||||
func (handler *Handler) deploykeyList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
deploykeys, err := handler.DeploykeyService.Deploykeys()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve deploykeys from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, deploykeys)
|
||||
}
|
||||
35
api/http/handler/deploykeys/handler.go
Normal file
35
api/http/handler/deploykeys/handler.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle deploykey operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DeploykeyService portainer.DeploykeyService
|
||||
CryptoService portainer.CryptoService
|
||||
DigitalDeploykeyService portainer.DigitalDeploykeyService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage deploykey operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/deploykeys",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploykeyCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/deploykeys",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploykeyList))).Methods(http.MethodGet)
|
||||
h.Handle("/deploykeys/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploykeyDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/http/handler/auth"
|
||||
"github.com/portainer/portainer/http/handler/deploykeys"
|
||||
"github.com/portainer/portainer/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/http/handler/endpointproxy"
|
||||
@@ -49,6 +50,7 @@ type Handler struct {
|
||||
UserHandler *users.Handler
|
||||
WebSocketHandler *websocket.Handler
|
||||
WebhookHandler *webhooks.Handler
|
||||
DeploykeyHandler *deploykeys.Handler
|
||||
}
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
@@ -87,6 +89,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/deploykeys"):
|
||||
http.StripPrefix("/api", h.DeploykeyHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/upload"):
|
||||
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/users"):
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/portainer/portainer/docker"
|
||||
"github.com/portainer/portainer/http/handler"
|
||||
"github.com/portainer/portainer/http/handler/auth"
|
||||
"github.com/portainer/portainer/http/handler/deploykeys"
|
||||
"github.com/portainer/portainer/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/http/handler/endpointproxy"
|
||||
@@ -48,6 +49,7 @@ type Server struct {
|
||||
DockerHubService portainer.DockerHubService
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
DigitalDeploykeyService portainer.DigitalDeploykeyService
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
JWTService portainer.JWTService
|
||||
@@ -62,6 +64,7 @@ type Server struct {
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
TemplateService portainer.TemplateService
|
||||
UserService portainer.UserService
|
||||
DeploykeyService portainer.DeploykeyService
|
||||
WebhookService portainer.WebhookService
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
@@ -152,6 +155,10 @@ func (server *Server) Start() error {
|
||||
teamHandler.TeamService = server.TeamService
|
||||
teamHandler.TeamMembershipService = server.TeamMembershipService
|
||||
|
||||
var deploykeyHandler = deploykeys.NewHandler(requestBouncer)
|
||||
deploykeyHandler.DeploykeyService = server.DeploykeyService
|
||||
deploykeyHandler.DigitalDeploykeyService = server.DigitalDeploykeyService
|
||||
|
||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||
@@ -201,6 +208,7 @@ func (server *Server) Start() error {
|
||||
UserHandler: userHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
WebhookHandler: webhookHandler,
|
||||
DeploykeyHandler: deploykeyHandler,
|
||||
}
|
||||
|
||||
if server.SSL {
|
||||
|
||||
@@ -321,6 +321,19 @@ type (
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
||||
}
|
||||
|
||||
// DeploykeyID represents a key identifier
|
||||
DeploykeyID int
|
||||
|
||||
// Deploykey represents a key that can be associated to a resource
|
||||
Deploykey struct {
|
||||
ID DeploykeyID
|
||||
Name string `json:"Name"`
|
||||
Privatekeypath string `json:"Privatekeypath"`
|
||||
Publickeypath string `json:"Publickeypath"`
|
||||
UserID int `json:"UserID"`
|
||||
LastUsage string `json:"LastUsage"`
|
||||
}
|
||||
|
||||
// TagID represents a tag identifier
|
||||
TagID int
|
||||
|
||||
@@ -450,6 +463,13 @@ type (
|
||||
DeleteTeam(ID TeamID) error
|
||||
}
|
||||
|
||||
// DeploykeyService represents a service for managing key data
|
||||
DeploykeyService interface {
|
||||
Deploykeys() ([]Deploykey, error)
|
||||
CreateDeploykey(deploykey *Deploykey) error
|
||||
DeleteDeploykey(ID DeploykeyID) error
|
||||
}
|
||||
|
||||
// TeamMembershipService represents a service for managing team membership data
|
||||
TeamMembershipService interface {
|
||||
TeamMembership(ID TeamMembershipID) (*TeamMembership, error)
|
||||
@@ -572,6 +592,16 @@ type (
|
||||
Sign(message string) (string, error)
|
||||
}
|
||||
|
||||
//DigitalDeploykeyService represents a service to manage digital deploykey
|
||||
DigitalDeploykeyService interface {
|
||||
ParseKeyPair(private, public []byte) error
|
||||
GenerateKeyPair() ([]byte, []byte, error)
|
||||
GenerateSshKey()([]byte, error)
|
||||
EncodedPublicKey() string
|
||||
PEMHeaders() (string, string)
|
||||
Sign(message string) (string, error)
|
||||
}
|
||||
|
||||
// JWTService represents a service for managing JWT tokens
|
||||
JWTService interface {
|
||||
GenerateToken(data *TokenData) (string, error)
|
||||
|
||||
161
api/swagger.yaml
161
api/swagger.yaml
@@ -61,6 +61,8 @@ info:
|
||||
host: "portainer.domain"
|
||||
basePath: "/api"
|
||||
tags:
|
||||
- name: "deploykeys"
|
||||
description: "Manage deploykeys"
|
||||
- name: "auth"
|
||||
description: "Authenticate against Portainer HTTP API"
|
||||
- name: "dockerhub"
|
||||
@@ -2311,6 +2313,105 @@ paths:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/deploykeys:
|
||||
get:
|
||||
tags:
|
||||
- "deploykeys"
|
||||
summary: "List deploykeys"
|
||||
description: |
|
||||
List deploykeys.
|
||||
**Access policy**: administrator
|
||||
operationId: "DeploykeysList"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/DeploykeyListResponse"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
404:
|
||||
description: "Key not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
post:
|
||||
tags:
|
||||
- "deploykeys"
|
||||
summary: "Create a new deploykeys"
|
||||
description: |
|
||||
Create a new deploykeys.
|
||||
**Access policy**: administrator
|
||||
operationId: "DeploykeyCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Deploykey details"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/DeploykeyCreateRequest"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/Deploykey"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
409:
|
||||
description: "Conflict"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "A deploykeys with the specified name already exists"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/deploykeys/{id}:
|
||||
delete:
|
||||
tags:
|
||||
- "deploykeys"
|
||||
summary: "Remove a deploykeys"
|
||||
description: |
|
||||
Remove a deploykeys.
|
||||
**Access policy**: administrator
|
||||
operationId: "DeploykeyDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Deploykey identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
responses:
|
||||
204:
|
||||
description: "Success"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/teams/{id}/memberships:
|
||||
get:
|
||||
@@ -2735,6 +2836,35 @@ definitions:
|
||||
type: "string"
|
||||
example: "org/acme"
|
||||
description: "Tag name"
|
||||
|
||||
Deploykey:
|
||||
type: "object"
|
||||
properties:
|
||||
Id:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Deploykey identifier"
|
||||
Name:
|
||||
type: "string"
|
||||
example: "abcd"
|
||||
description: "Deploykey name"
|
||||
Privatekeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey private key path"
|
||||
Publickeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey public key path"
|
||||
UserID:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Deploykey user id"
|
||||
LastUsage:
|
||||
type: "string"
|
||||
example: "12:03:32"
|
||||
description: " last usage deploykey date"
|
||||
|
||||
Team:
|
||||
type: "object"
|
||||
properties:
|
||||
@@ -3717,6 +3847,37 @@ definitions:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/TeamMembership"
|
||||
DeploykeyListResponse:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Deploykey"
|
||||
|
||||
DeploykeyCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "Name"
|
||||
properties:
|
||||
Name:
|
||||
type: "string"
|
||||
example: "abcd"
|
||||
description: "Deploykey name"
|
||||
Privatekeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey private key path"
|
||||
Publickeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey public key path"
|
||||
UserID:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Deploykey user id"
|
||||
LastUsage:
|
||||
type: "string"
|
||||
example: "12:03:32"
|
||||
description: "last usage deploykey date"
|
||||
|
||||
|
||||
TeamMembershipCreateRequest:
|
||||
type: "object"
|
||||
|
||||
@@ -10,6 +10,7 @@ angular.module('portainer')
|
||||
.constant('API_ENDPOINT_STACKS', 'api/stacks')
|
||||
.constant('API_ENDPOINT_STATUS', 'api/status')
|
||||
.constant('API_ENDPOINT_USERS', 'api/users')
|
||||
.constant('API_ENDPOINT_DEPLOYKEYS', 'api/deploykeys')
|
||||
.constant('API_ENDPOINT_TAGS', 'api/tags')
|
||||
.constant('API_ENDPOINT_TEAMS', 'api/teams')
|
||||
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
|
||||
|
||||
@@ -318,6 +318,17 @@ angular.module('portainer.app', [])
|
||||
}
|
||||
};
|
||||
|
||||
var deploykeys = {
|
||||
name: 'portainer.deploykeys',
|
||||
url: '/deploykeys',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/deploykeys/deploykeys.html',
|
||||
controller: 'DeploykeysController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var updatePassword = {
|
||||
name: 'portainer.updatePassword',
|
||||
url: '/update-password',
|
||||
@@ -434,6 +445,7 @@ angular.module('portainer.app', [])
|
||||
$stateRegistryProvider.register(stack);
|
||||
$stateRegistryProvider.register(stackCreation);
|
||||
$stateRegistryProvider.register(support);
|
||||
$stateRegistryProvider.register(deploykeys);
|
||||
$stateRegistryProvider.register(tags);
|
||||
$stateRegistryProvider.register(updatePassword);
|
||||
$stateRegistryProvider.register(users);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
angular.module('portainer.app').component('porStackauthControlForm', {
|
||||
templateUrl: 'app/portainer/components/StackAuthControlForm/porStackauthControlForm.html',
|
||||
controller: 'porStackauthControlFormController',
|
||||
bindings: {
|
||||
// This object will be populated with the form data.
|
||||
// Model reference in porStackauthControlFromModel.js
|
||||
formData: '=',
|
||||
// Optional. An existing resource control object that will be used to set
|
||||
// the default values of the component.
|
||||
resourceControl: '<'
|
||||
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
<div style="margin-bottom: 0">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-3">
|
||||
<label for="stack_sshrepository_path" class="col-sm-12 control-label text-left">Select deploy key</label>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-12">
|
||||
<span ng-class="{ 'pull-right': $ctrl.formData.showAddAction }" style="width: 25%;">
|
||||
<ui-select ng-model="$ctrl.formData.GenrateSshkey.selected" on-select="$ctrl.formData.change($select.selected.Publickeypath)">
|
||||
<ui-select-match placeholder="Select a deploy key"> <!-- allow-clear="true" -->
|
||||
<span>{{ $select.selected.Name }}</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="category as category in ($ctrl.formData.GenrateSshkey | filter: $select.search)" >
|
||||
<span>{{ category.Name }}</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</span>
|
||||
<input style="display: none;" id="selPKey" value="{{$ctrl.formData.GenrateSshkey.selected.Publickeypath}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="col-sm-5">
|
||||
<button disabled type="button" class="btn btn-primary btn-sm copyToClip" ng-click="$ctrl.formData.copytoclipboard($ctrl.formData.GenrateSshkey.selected)">
|
||||
<i class="fas fa-key space-right" aria-hidden="true"></i>Copy to clipboard
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<span>
|
||||
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<button type="button" class="btn btn-primary btn-sm" disabled id="btngeneratekey" ng-click="$ctrl.formData.createNewkey()">
|
||||
<i class="fas fa-key space-right" aria-hidden="true"></i>Generate a new deploy key
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p id="warningStackname" style="margin-left: 10px; float: left;"><i class="fa fa-exclamation-triangle text-warning"></i> Please specify a stack name.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
angular.module('portainer.app')
|
||||
.controller('porStackauthControlFormController', ['$q', '$state','$scope','DeploykeyService','StackService', 'UserService', 'TeamService', 'Notifications', 'Authentication', 'ResourceControlService',
|
||||
function ($q,$state,$scope, DeploykeyService, StackService, UserService, TeamService, Notifications, Authentication, ResourceControlService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.formData.existingsshKey = [];
|
||||
ctrl.formData.newgenratedeploykey = [];
|
||||
ctrl.enableGenratekey = false;
|
||||
|
||||
ctrl.formData.copytoclipboard = function (){
|
||||
var $temp = $("<input>");
|
||||
$("body").append($temp);
|
||||
$temp.val($('#selPKey').val()).select();
|
||||
document.execCommand("copy");
|
||||
$temp.remove();
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(2000);
|
||||
|
||||
}
|
||||
|
||||
ctrl.formData.change = function(key){
|
||||
if(key != ''){
|
||||
$('.copyToClip').removeAttr("disabled")
|
||||
}
|
||||
else{
|
||||
$('.copyToClip').attr('disabled','disabled');
|
||||
}
|
||||
}
|
||||
|
||||
ctrl.formData.createNewkey = function() {
|
||||
if(StackService.getStackName() == ''){
|
||||
$('#stack_name').focus();
|
||||
} else {
|
||||
var createKeyName = 'deploykey_' + StackService.getStackName();
|
||||
var userID = Authentication.getUserDetails().ID;
|
||||
DeploykeyService.createNewdeploykey(createKeyName,userID)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Key successfully created', createKeyName);
|
||||
//$state.reload();
|
||||
getAlldeploykeydata(data)
|
||||
$('.copyToClip').removeAttr("disabled")
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create key');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initComponent() {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true: false;
|
||||
ctrl.isAdmin = isAdmin;
|
||||
|
||||
if (isAdmin) {
|
||||
ctrl.formData.Ownership = 'administrators';
|
||||
}
|
||||
|
||||
/*Enable / Disable deploykey button based on text entering in stack name textbox*/
|
||||
if($('#stack_name').val() !=""){
|
||||
$('#btngeneratekey').removeAttr("disabled")
|
||||
$('#warningStackname').hide();
|
||||
} else {
|
||||
$('#btngeneratekey').attr('disabled','disabled');
|
||||
$('#warningStackname').show();
|
||||
}
|
||||
|
||||
DeploykeyService.deploykeys()
|
||||
.then(function success(data) {
|
||||
ctrl.formData.GenrateSshkey = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve keys');
|
||||
ctrl.formData.GenrateSshkey = [];
|
||||
});
|
||||
}
|
||||
|
||||
function getAlldeploykeydata(getdata){
|
||||
DeploykeyService.deploykeys()
|
||||
.then(function success(data) {
|
||||
ctrl.formData.GenrateSshkey = data;
|
||||
ctrl.formData.GenrateSshkey.selected = getdata;//{value : ctrl.formData.GenrateSshkey[data.length - 1]}
|
||||
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve keys');
|
||||
ctrl.formData.GenrateSshkey = [];
|
||||
});
|
||||
}
|
||||
|
||||
initComponent();
|
||||
}]);
|
||||
@@ -0,0 +1,6 @@
|
||||
function StackAuthControlFormData() {
|
||||
this.StackAuthControlEnabled = true;
|
||||
this.Ownership = 'private';
|
||||
this.ExistingsshKey = [];
|
||||
this.GenrateSshkey = [];
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fas fa-key" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>Action</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<td class="col-sm-3">
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
{{ item.Name }}
|
||||
</td>
|
||||
<td class="col-sm-3">
|
||||
<input style="display: none;" type="text" id="pubkey{{ $index }}" class="form-control" ng-model="rootFolders" ng-init="rootFolders='{{ item.Publickeypath }}'" value="{{ item.Publickeypath }}"/>
|
||||
<button type="button" class="btn btn-sm btn-information" ng-click = "$ctrl.copyToClipboard('pubkey'+$index, $index)">
|
||||
<i class="fas fa-key space-right" aria-hidden="true"></i>Copy to clipboard
|
||||
</button>
|
||||
<span>
|
||||
<i id="refreshRateChange{{ $index }}" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="1" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="1" class="text-center text-muted">No key available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||
</div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
angular.module('portainer.app').component('deploykeysDatatable', {
|
||||
templateUrl: 'app/portainer/components/datatables/deploykeys-datatable/deploykeysDatatable.html',
|
||||
controller: 'deploykeysDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
angular.module('portainer.app')
|
||||
.controller('deploykeysDatatableController', ['PaginationService', 'DatatableService',
|
||||
function (PaginationService, DatatableService) {
|
||||
|
||||
this.state = {
|
||||
selectAll: false,
|
||||
orderBy: this.orderBy,
|
||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
||||
displayTextFilter: false,
|
||||
selectedItemCount: 0,
|
||||
selectedItems: []
|
||||
};
|
||||
|
||||
this.changeOrderBy = function(orderField) {
|
||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||
this.state.orderBy = orderField;
|
||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
||||
};
|
||||
|
||||
this.selectItem = function(item) {
|
||||
if (item.Checked) {
|
||||
this.state.selectedItemCount++;
|
||||
this.state.selectedItems.push(item);
|
||||
} else {
|
||||
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
|
||||
this.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
this.selectAll = function() {
|
||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
||||
var item = this.state.filteredDataSet[i];
|
||||
if (!(item.External && item.Type === 2) && item.Checked !== this.state.selectAll) {
|
||||
item.Checked = this.state.selectAll;
|
||||
this.selectItem(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.changePaginationLimit = function() {
|
||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
||||
};
|
||||
|
||||
this.$onInit = function() {
|
||||
setDefaults(this);
|
||||
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
this.state.orderBy = storedOrder.orderBy;
|
||||
}
|
||||
};
|
||||
|
||||
function setDefaults(ctrl) {
|
||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
||||
}
|
||||
|
||||
this.copyToClipboard = function(cpotyText, indexNo){
|
||||
var $temp = $("<input>");
|
||||
$("body").append($temp);
|
||||
$temp.val($('#'+cpotyText).val()).select();
|
||||
document.execCommand("copy");
|
||||
$temp.remove();
|
||||
$('#refreshRateChange'+indexNo).show();
|
||||
$('#refreshRateChange'+indexNo).fadeOut(2000);
|
||||
}
|
||||
}]);
|
||||
9
app/portainer/models/deploykey.js
Normal file
9
app/portainer/models/deploykey.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function DeploykeyViewModel(data) {
|
||||
this.Id = data.ID;
|
||||
this.Name = data.Name;
|
||||
this.Privatekeypath = data.Privatekeypath;
|
||||
this.Publickeypath = data.Publickeypath;
|
||||
this.UserID = data.UserID;
|
||||
this.lastUsage = data.LastUsage;
|
||||
}
|
||||
|
||||
9
app/portainer/rest/deploykey.js
Normal file
9
app/portainer/rest/deploykey.js
Normal file
@@ -0,0 +1,9 @@
|
||||
angular.module('portainer.app')
|
||||
.factory('Deploykeys', ['$resource', 'API_ENDPOINT_DEPLOYKEYS', function DeploykeysFactory($resource, API_ENDPOINT_DEPLOYKEYS) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_DEPLOYKEYS + '/:id/:entity/:entityId', {}, {
|
||||
create: { method: 'POST' },
|
||||
query: { method: 'GET', isArray: true },
|
||||
remove: { method: 'DELETE', params: { id: '@id'} }
|
||||
});
|
||||
}]);
|
||||
49
app/portainer/services/api/deploykeyService.js
Normal file
49
app/portainer/services/api/deploykeyService.js
Normal file
@@ -0,0 +1,49 @@
|
||||
angular.module('portainer.app')
|
||||
.factory('DeploykeyService', ['$q', 'Deploykeys', function DeploykeyServiceFactory($q, Deploykeys) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.deploykeys = function() {
|
||||
var deferred = $q.defer();
|
||||
Deploykeys.query().$promise
|
||||
.then(function success(data) {
|
||||
var deploykeys = data.map(function (item) {
|
||||
return new DeploykeyViewModel(item);
|
||||
});
|
||||
deferred.resolve(deploykeys);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({msg: 'Unable to retrieve keys', err: err});
|
||||
});
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.createNewkeyNames = function() {
|
||||
var deferred = $q.defer();
|
||||
Deploykeys.query().$promise
|
||||
.then(function success(data) {
|
||||
var deploykeys = data.map(function (item) {
|
||||
return item.Name;
|
||||
});
|
||||
deferred.resolve(deploykeys);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({msg: 'Unable to retrieve keys', err: err});
|
||||
});
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.createNewdeploykey = function(name,UserID) {
|
||||
var payload = {
|
||||
Name: name,
|
||||
UserID: UserID
|
||||
};
|
||||
return Deploykeys.create({}, payload).$promise;
|
||||
};
|
||||
|
||||
service.deleteNewdeploykey = function(id) {
|
||||
return Deploykeys.remove({id: id}).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
@@ -4,6 +4,13 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
var stackName='';
|
||||
service.setStackName = function(sackname){
|
||||
stackName = sackname;
|
||||
}
|
||||
service.getStackName = function(){
|
||||
return stackName;
|
||||
}
|
||||
|
||||
service.stack = function(id) {
|
||||
var deferred = $q.defer();
|
||||
@@ -34,7 +41,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.migrateSwarmStack = function(stack, targetEndpointId, newName) {
|
||||
service.migrateSwarmStack = function(stack, targetEndpointId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
EndpointProvider.setEndpointID(targetEndpointId);
|
||||
@@ -46,7 +53,8 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||
deferred.reject({ msg: 'Target endpoint is located in the same Swarm cluster as the current endpoint', err: null });
|
||||
return;
|
||||
}
|
||||
return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id, Name: newName }).$promise;
|
||||
|
||||
return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id }).$promise;
|
||||
})
|
||||
.then(function success() {
|
||||
deferred.resolve();
|
||||
@@ -61,12 +69,12 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.migrateComposeStack = function(stack, targetEndpointId, newName) {
|
||||
service.migrateComposeStack = function(stack, targetEndpointId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
EndpointProvider.setEndpointID(targetEndpointId);
|
||||
|
||||
Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, Name: newName }).$promise
|
||||
Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId }).$promise
|
||||
.then(function success() {
|
||||
deferred.resolve();
|
||||
})
|
||||
@@ -258,7 +266,8 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||
var deferred = $q.defer();
|
||||
|
||||
SwarmService.swarm()
|
||||
.then(function success(swarm) {
|
||||
.then(function success(data) {
|
||||
var swarm = data;
|
||||
var payload = {
|
||||
Name: name,
|
||||
SwarmID: swarm.Id,
|
||||
@@ -277,23 +286,26 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.createComposeStackFromGitRepository = function(name, repositoryOptions, env, endpointId) {
|
||||
service.createComposeStackFromGitRepository = function(name, repositoryOptions, env, endpointId) {
|
||||
var payload = {
|
||||
Name: name,
|
||||
RepositoryURL: repositoryOptions.RepositoryURL,
|
||||
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: repositoryOptions.ComposeFilePathInRepository,
|
||||
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
|
||||
stackAuthenticationControlEnabled: repositoryOptions.stackAuthenticationControlEnabled,
|
||||
RepositoryUsername: repositoryOptions.RepositoryUsername,
|
||||
RepositoryPassword: repositoryOptions.RepositoryPassword,
|
||||
RepositoryPrivatekeypath: repositoryOptions.RepositoryPrivatekeypath,
|
||||
RepositoryPublickeypath: repositoryOptions.RepositoryPublickeypath,
|
||||
Env: env
|
||||
};
|
||||
};
|
||||
|
||||
return Stack.create({ method: 'repository', type: 2, endpointId: endpointId }, payload).$promise;
|
||||
};
|
||||
|
||||
service.createSwarmStackFromGitRepository = function(name, repositoryOptions, env, endpointId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
SwarmService.swarm()
|
||||
.then(function success(data) {
|
||||
var swarm = data;
|
||||
@@ -304,10 +316,14 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: repositoryOptions.ComposeFilePathInRepository,
|
||||
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
|
||||
stackAuthenticationControlEnabled: repositoryOptions.stackAuthenticationControlEnabled,
|
||||
RepositoryUsername: repositoryOptions.RepositoryUsername,
|
||||
RepositoryPassword: repositoryOptions.RepositoryPassword,
|
||||
RepositoryPassword: repositoryOptions.RepositoryPassword,
|
||||
RepositoryPrivatekeypath: repositoryOptions.RepositoryPrivatekeypath,
|
||||
RepositoryPublickeypath: repositoryOptions.RepositoryPublickeypath,
|
||||
Env: env
|
||||
};
|
||||
|
||||
return Stack.create({ method: 'repository', type: 1, endpointId: endpointId }, payload).$promise;
|
||||
})
|
||||
.then(function success(data) {
|
||||
@@ -320,10 +336,5 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.duplicateStack = function duplicateStack(name, stackFileContent, env, endpointId, type) {
|
||||
var action = type === 1 ? service.createSwarmStackFromFileContent : service.createComposeStackFromFileContent;
|
||||
return action(name, stackFileContent, env, endpointId);
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
||||
59
app/portainer/views/deploykeys/deploykeys.html
Normal file
59
app/portainer/views/deploykeys/deploykeys.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Deploy Keys">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.deploykeys" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Deploy Keys management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title-text="Add a new key">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="deploykeyCreationForm" ng-submit="createDeploykey()">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-sm-2 control-label text-left">
|
||||
Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" name="name" ng-model="formValues.Name" ng-change="checkNameValidity(deploykeyCreationForm)" placeholder="org/acme" required auto-focus />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="deploykeyCreationForm.name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="deploykeyCreationForm.name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="validName"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This key already exists.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !deploykeyCreationForm.$valid" ng-click="createDeploykey()" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Create key</span>
|
||||
<span ng-show="state.actionInProgress">Creating key...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<deploykeys-datatable
|
||||
title-text="Keys" title-icon="fa-tags"
|
||||
dataset="deploykeys" table-key="deploykeys"
|
||||
order-by="Name"
|
||||
remove-action="removeAction"
|
||||
></deploykeys-datatable>
|
||||
</div>
|
||||
</div>
|
||||
71
app/portainer/views/deploykeys/deploykeysController.js
Normal file
71
app/portainer/views/deploykeys/deploykeysController.js
Normal file
@@ -0,0 +1,71 @@
|
||||
angular.module('portainer.app')
|
||||
.controller('DeploykeysController', ['$scope', '$state', 'DeploykeyService', 'Notifications', 'Authentication',
|
||||
function ($scope, $state, DeploykeyService, Notifications, Authentication) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Name: ''
|
||||
};
|
||||
|
||||
$scope.checkNameValidity = function(form) {
|
||||
var valid = true;
|
||||
for (var i = 0; i < $scope.deploykeys.length; i++) {
|
||||
if ($scope.formValues.Name === $scope.deploykeys[i].Name) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
form.name.$setValidity('validName', valid);
|
||||
};
|
||||
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
var actionCount = selectedItems.length;
|
||||
|
||||
angular.forEach(selectedItems, function (deploykey) {
|
||||
DeploykeyService.deleteNewdeploykey(deploykey.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Key successfully removed', deploykey.Name);
|
||||
var index = $scope.deploykeys.indexOf(deploykey);
|
||||
$scope.deploykeys.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to key');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createDeploykey = function() {
|
||||
var deploykeyName = $scope.formValues.Name;
|
||||
var UserID = Authentication.getUserDetails().ID;
|
||||
DeploykeyService.createNewdeploykey(deploykeyName,UserID)
|
||||
.then(function success() {
|
||||
Notifications.success('Key successfully created', deploykeyName);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create key');
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
DeploykeyService.deploykeys()
|
||||
.then(function success(data) {
|
||||
$scope.deploykeys = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve keys');
|
||||
$scope.deploykeys = [];
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
@@ -64,6 +64,9 @@
|
||||
<a ui-sref="portainer.about" ui-sref-active="active">About</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||
<a ui-sref="portainer.deploykeys" ui-sref-active="active">Deploy Keys <span class="menu-icon fas fa-key"></span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-footer-content">
|
||||
<img src="images/logo_small.png" class="img-responsive logo" alt="Portainer">
|
||||
|
||||
@@ -7,14 +7,32 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
|
||||
StackFileContent: '',
|
||||
StackFile: null,
|
||||
RepositoryURL: '',
|
||||
RepositoryReferenceName: '',
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryReferenceName: "",
|
||||
RepositoryUsername: '',
|
||||
RepositoryPassword: '',
|
||||
Env: [],
|
||||
ComposeFilePathInRepository: 'docker-compose.yml',
|
||||
AccessControlData: new AccessControlFormData()
|
||||
};
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
StachauthControlData: new StackAuthControlFormData(),
|
||||
RepositoryAuthentication:false,
|
||||
stackAuthenticationControlEnabled:''
|
||||
};
|
||||
var stateChangeKey = false;
|
||||
$scope.formValues.generateNewKey =function(){
|
||||
StackService.setStackName($scope.formValues.Name);
|
||||
if($('#stack_name').val() !=""){
|
||||
$('#btngeneratekey').removeAttr("disabled")
|
||||
$('#warningStackname').hide();
|
||||
} else {
|
||||
$('#btngeneratekey').attr('disabled','disabled');
|
||||
$('#warningStackname').show();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.formValues.authenticationEnable =function (vale){
|
||||
$scope.formValues.stackAuthenticationControlEnabled = '1';
|
||||
stateChangeKey = vale;
|
||||
}
|
||||
|
||||
$scope.state = {
|
||||
Method: 'editor',
|
||||
@@ -33,7 +51,7 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
$scope.state.formValidationError = '';
|
||||
var error = '';
|
||||
var error = '';
|
||||
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
||||
if (error) {
|
||||
@@ -44,47 +62,88 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
|
||||
}
|
||||
|
||||
function createSwarmStack(name, method) {
|
||||
|
||||
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
||||
var endpointId = EndpointProvider.endpointID();
|
||||
|
||||
var StachauthControlData = $scope.formValues.StachauthControlData;
|
||||
|
||||
if (method === 'editor') {
|
||||
var stackFileContent = $scope.formValues.StackFileContent;
|
||||
return StackService.createSwarmStackFromFileContent(name, stackFileContent, env, endpointId);
|
||||
} else if (method === 'upload') {
|
||||
var stackFile = $scope.formValues.StackFile;
|
||||
return StackService.createSwarmStackFromFileUpload(name, stackFile, env, endpointId);
|
||||
} else if (method === 'repository') {
|
||||
} else if (method === 'repository') {
|
||||
|
||||
var prKey = "";
|
||||
var pubKey = "";
|
||||
|
||||
if($scope.formValues.stackAuthenticationControlEnabled == 2){
|
||||
prKey = "";
|
||||
pubKey = ""
|
||||
|
||||
} else if($scope.formValues.stackAuthenticationControlEnabled == 1) {
|
||||
prKey = StachauthControlData.GenrateSshkey['selected'].Privatekeypath;
|
||||
pubKey = StachauthControlData.GenrateSshkey['selected'].Publickeypath;
|
||||
}
|
||||
|
||||
var repositoryOptions = {
|
||||
RepositoryURL: $scope.formValues.RepositoryURL,
|
||||
RepositoryReferenceName: $scope.formValues.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: $scope.formValues.ComposeFilePathInRepository,
|
||||
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
|
||||
RepositoryAuthentication : $scope.formValues.RepositoryAuthentication,
|
||||
stackAuthenticationControlEnabled : stateChangeKey,
|
||||
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
||||
RepositoryPassword: $scope.formValues.RepositoryPassword
|
||||
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
||||
RepositoryPrivatekeypath: prKey,
|
||||
RepositoryPublickeypath: pubKey
|
||||
|
||||
};
|
||||
|
||||
return StackService.createSwarmStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
||||
}
|
||||
}
|
||||
|
||||
function createComposeStack(name, method) {
|
||||
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
||||
function createComposeStack(name, method) {
|
||||
|
||||
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
||||
var endpointId = EndpointProvider.endpointID();
|
||||
|
||||
|
||||
var StachauthControlData = $scope.formValues.StachauthControlData;
|
||||
|
||||
if (method === 'editor') {
|
||||
var stackFileContent = $scope.formValues.StackFileContent;
|
||||
return StackService.createComposeStackFromFileContent(name, stackFileContent, env, endpointId);
|
||||
} else if (method === 'upload') {
|
||||
var stackFile = $scope.formValues.StackFile;
|
||||
return StackService.createComposeStackFromFileUpload(name, stackFile, env, endpointId);
|
||||
} else if (method === 'repository') {
|
||||
} else if (method === 'repository') {
|
||||
|
||||
var prKey = "";
|
||||
var pubKey = "";
|
||||
|
||||
if($scope.formValues.stackAuthenticationControlEnabled == 2){
|
||||
prKey = "";
|
||||
pubKey = ""
|
||||
|
||||
} else if($scope.formValues.stackAuthenticationControlEnabled == 1) {
|
||||
prKey = StachauthControlData.GenrateSshkey['selected'].Privatekeypath;
|
||||
pubKey = StachauthControlData.GenrateSshkey['selected'].Publickeypath;
|
||||
}
|
||||
|
||||
var repositoryOptions = {
|
||||
RepositoryURL: $scope.formValues.RepositoryURL,
|
||||
RepositoryReferenceName: $scope.formValues.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: $scope.formValues.ComposeFilePathInRepository,
|
||||
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
|
||||
ComposeFilePathInRepository: $scope.formValues.ComposeFilePathInRepository,
|
||||
RepositoryAuthentication : $scope.formValues.RepositoryAuthentication,
|
||||
stackAuthenticationControlEnabled : stateChangeKey,
|
||||
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
||||
RepositoryPassword: $scope.formValues.RepositoryPassword
|
||||
};
|
||||
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
||||
RepositoryPrivatekeypath: prKey,
|
||||
RepositoryPublickeypath: pubKey
|
||||
};
|
||||
|
||||
return StackService.createComposeStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
||||
}
|
||||
}
|
||||
@@ -94,6 +153,7 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
|
||||
var method = $scope.state.Method;
|
||||
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1;
|
||||
var userId = userDetails.ID;
|
||||
@@ -136,6 +196,7 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
|
||||
function initView() {
|
||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||
$scope.state.StackType = 2;
|
||||
|
||||
if (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER') {
|
||||
$scope.state.StackType = 1;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,20 @@
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<form class="form-horizontal" name="stackCreationForm">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<div class="form-group" ng-class="{ 'has-error': stackCreationForm.name.$invalid }">
|
||||
<label for="stack_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="formValues.Name" id="stack_name" placeholder="e.g. myStack" auto-focus>
|
||||
<input type="text" class="form-control" ng-model="formValues.Name" name="name" id="stack_name" ng-change="formValues.generateNewKey()" placeholder="e.g. myStack" required auto-focus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="stackCreationForm.name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="stackCreationForm.name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="validName"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This tag already exists.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
@@ -147,28 +155,71 @@
|
||||
<input type="text" class="form-control" ng-model="formValues.ComposeFilePathInRepository" id="stack_repository_path" placeholder="docker-compose.yml">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
<div>
|
||||
<!-- access-control-switch -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Authentication
|
||||
<label for="userOwnership" class="control-label text-left">
|
||||
Authentication
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="formValues.RepositoryAuthentication"><i></i>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input name="userOwnership" type="checkbox" ng-model="formValues.RepositoryAuthentication" ng-change="formValues.authenticationEnable(formValues.RepositoryAuthentication)"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="formValues.RepositoryAuthentication">
|
||||
<label for="repository_username" class="col-sm-1 control-label text-left">Username</label>
|
||||
<div class="col-sm-11 col-md-5">
|
||||
<input type="text" class="form-control" ng-model="formValues.RepositoryUsername" name="repository_username" placeholder="myGitUser">
|
||||
</div>
|
||||
<label for="repository_password" class="col-sm-1 margin-sm-top control-label text-left">
|
||||
Password
|
||||
</label>
|
||||
<div class="col-sm-11 col-md-5 margin-sm-top">
|
||||
<input type="password" class="form-control" ng-model="formValues.RepositoryPassword" name="repository_password" placeholder="myPassword">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0" ng-if="formValues.RepositoryAuthentication">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="registry_custom" ng-model="formValues.stackAuthenticationControlEnabled" ng-value="1">
|
||||
<label for="registry_custom">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fas fa-key" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Deploy key
|
||||
</div>
|
||||
<p>
|
||||
Use a deploy key
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="registry_quay" ng-model="formValues.stackAuthenticationControlEnabled" ng-value="2">
|
||||
<label for="registry_quay">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fas fa-unlock" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Credentials
|
||||
</div>
|
||||
<p>
|
||||
Username and password authentication
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- user and password -->
|
||||
<div class="form-group" ng-if="formValues.stackAuthenticationControlEnabled == '2' && formValues.RepositoryAuthentication">
|
||||
<label for="repository_username" class="col-sm-1 control-label text-left">Username</label>
|
||||
<div class="col-sm-11 col-md-5">
|
||||
<input type="text" class="form-control" ng-model="formValues.RepositoryUsername" name="repository_username" placeholder="myGitUser">
|
||||
</div>
|
||||
<label for="repository_password" class="col-sm-1 margin-sm-top control-label text-left">
|
||||
Password
|
||||
</label>
|
||||
<div class="col-sm-11 col-md-5 margin-sm-top">
|
||||
<input type="password" class="form-control" ng-model="formValues.RepositoryPassword" name="repository_password" placeholder="myPassword">
|
||||
</div>
|
||||
</div>
|
||||
<!-- end user and passowrd-->
|
||||
|
||||
<!-- SSH Authentication list -->
|
||||
<div class="form-group">
|
||||
<por-stackauth-control-form form-data="formValues.StachauthControlData" ng-if="formValues.stackAuthenticationControlEnabled == '1' && formValues.RepositoryAuthentication"></por-stackauth-control-form>
|
||||
</div>
|
||||
|
||||
<!-- end SSH Authentication -->
|
||||
<!-- !authorized-users -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- environment-variables -->
|
||||
<div>
|
||||
@@ -204,6 +255,7 @@
|
||||
<!-- !environment-variables -->
|
||||
<!-- !repository -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
@@ -211,8 +263,8 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress
|
||||
|| (state.Method === 'upload' && !formValues.StackFile)
|
||||
|| (state.Method === 'repository' && ((!formValues.RepositoryURL || !formValues.ComposeFilePathInRepository) || (formValues.RepositoryAuthentication && (!formValues.RepositoryUsername || !formValues.RepositoryPassword))))
|
||||
|| (state.Method === 'upload' && !formValues.StackFile)
|
||||
|| (state.Method === 'repository' && ((!formValues.RepositoryURL || !formValues.ComposeFilePathInRepository)))
|
||||
|| !formValues.Name" ng-click="deployStack()" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress">Deploy the stack</span>
|
||||
<span ng-show="state.actionInProgress">Deployment in progress...</span>
|
||||
|
||||
Reference in New Issue
Block a user