Compare commits

...

2 Commits

35 changed files with 3152 additions and 52 deletions

View File

@@ -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

View 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)
}

View File

@@ -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
View 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
}

View File

@@ -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
View 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
View 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):]
}

View File

@@ -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")

View 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[:])
}

View 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)
}

View 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)
}

View 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
}

View File

@@ -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"):

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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"

View File

@@ -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')

View File

@@ -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);

View File

@@ -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: '<'
}
});

View File

@@ -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>

View File

@@ -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();
}]);

View File

@@ -0,0 +1,6 @@
function StackAuthControlFormData() {
this.StackAuthControlEnabled = true;
this.Ownership = 'private';
this.ExistingsshKey = [];
this.GenrateSshkey = [];
}

View File

@@ -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>

View File

@@ -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: '<'
}
});

View File

@@ -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);
}
}]);

View 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;
}

View 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'} }
});
}]);

View 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;
}]);

View File

@@ -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;
}]);

View 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>

View 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();
}]);

View File

@@ -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">

View File

@@ -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;
}

View File

@@ -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>

757
yarn.lock

File diff suppressed because it is too large Load Diff