diff --git a/app/api/controller/user/controller.go b/app/api/controller/user/controller.go index 3f18dd448..ad59248c4 100644 --- a/app/api/controller/user/controller.go +++ b/app/api/controller/user/controller.go @@ -34,6 +34,7 @@ type Controller struct { principalStore store.PrincipalStore tokenStore store.TokenStore membershipStore store.MembershipStore + publicKeyStore store.PublicKeyStore } func NewController( @@ -43,6 +44,7 @@ func NewController( principalStore store.PrincipalStore, tokenStore store.TokenStore, membershipStore store.MembershipStore, + publicKeyStore store.PublicKeyStore, ) *Controller { return &Controller{ tx: tx, @@ -51,6 +53,7 @@ func NewController( principalStore: principalStore, tokenStore: tokenStore, membershipStore: membershipStore, + publicKeyStore: publicKeyStore, } } diff --git a/app/api/controller/user/publickey_create.go b/app/api/controller/user/publickey_create.go new file mode 100644 index 000000000..35681757a --- /dev/null +++ b/app/api/controller/user/publickey_create.go @@ -0,0 +1,119 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package user + +import ( + "context" + "fmt" + "strings" + "time" + + apiauth "github.com/harness/gitness/app/api/auth" + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/app/services/publickey" + "github.com/harness/gitness/errors" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" +) + +type CreatePublicKeyInput struct { + Identifier string `json:"identifier"` + Usage enum.PublicKeyUsage `json:"usage"` + Content string `json:"content"` +} + +func (c *Controller) CreatePublicKey( + ctx context.Context, + session *auth.Session, + userUID string, + in *CreatePublicKeyInput, +) (*types.PublicKey, error) { + user, err := c.principalStore.FindUserByUID(ctx, userUID) + if err != nil { + return nil, fmt.Errorf("failed to fetch user by uid: %w", err) + } + + if err = apiauth.CheckUser(ctx, c.authorizer, session, user, enum.PermissionUserEdit); err != nil { + return nil, err + } + + if err := sanitizeCreatePublicKeyInput(in); err != nil { + return nil, err + } + + key, comment, err := publickey.ParseString(in.Content) + if err != nil { + return nil, errors.InvalidArgument("could not parse public key") + } + + now := time.Now().UnixMilli() + + k := &types.PublicKey{ + PrincipalID: user.ID, + Created: now, + Verified: nil, // the key is created as unverified + Identifier: in.Identifier, + Usage: in.Usage, + Fingerprint: key.Fingerprint(), + Content: in.Content, + Comment: comment, + Type: key.Type(), + } + + err = c.tx.WithTx(ctx, func(ctx context.Context) error { + existingKeys, err := c.publicKeyStore.ListByFingerprint(ctx, k.Fingerprint) + if err != nil { + return fmt.Errorf("failed to read keys by fingerprint: %w", err) + } + + for _, existingKey := range existingKeys { + if key.Matches(existingKey.Content) { + return errors.InvalidArgument("Key is already in use") + } + } + + err = c.publicKeyStore.Create(ctx, k) + if err != nil { + return fmt.Errorf("failed to insert public key: %w", err) + } + + return nil + }) + if err != nil { + return nil, err + } + + return k, nil +} + +func sanitizeCreatePublicKeyInput(in *CreatePublicKeyInput) error { + if err := check.Identifier(in.Identifier); err != nil { + return err + } + + usage, ok := in.Usage.Sanitize() + if !ok { + return errors.InvalidArgument("invalid value for public key usage") + } + in.Usage = usage + + in.Content = strings.TrimSpace(in.Content) + if in.Content == "" { + return errors.InvalidArgument("public key not provided") + } + + return nil +} diff --git a/app/api/controller/user/publickey_delete.go b/app/api/controller/user/publickey_delete.go new file mode 100644 index 000000000..c907dd8f2 --- /dev/null +++ b/app/api/controller/user/publickey_delete.go @@ -0,0 +1,47 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package user + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/app/api/auth" + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) DeletePublicKey( + ctx context.Context, + session *auth.Session, + userUID string, + identifier string, +) error { + user, err := c.principalStore.FindUserByUID(ctx, userUID) + if err != nil { + return fmt.Errorf("failed to fetch user by uid: %w", err) + } + + if err = apiauth.CheckUser(ctx, c.authorizer, session, user, enum.PermissionUserEdit); err != nil { + return err + } + + err = c.publicKeyStore.DeleteByIdentifier(ctx, user.ID, identifier) + if err != nil { + return fmt.Errorf("failed to delete public key by id: %w", err) + } + + return nil +} diff --git a/app/api/controller/user/publickey_list.go b/app/api/controller/user/publickey_list.go new file mode 100644 index 000000000..0065b86c4 --- /dev/null +++ b/app/api/controller/user/publickey_list.go @@ -0,0 +1,71 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package user + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/app/api/auth" + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) ListPublicKeys( + ctx context.Context, + session *auth.Session, + userUID string, + filter *types.PublicKeyFilter, +) ([]types.PublicKey, int, error) { + user, err := c.principalStore.FindUserByUID(ctx, userUID) + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch user by uid: %w", err) + } + + if err = apiauth.CheckUser(ctx, c.authorizer, session, user, enum.PermissionUserView); err != nil { + return nil, 0, err + } + + var ( + list []types.PublicKey + count int + ) + + err = c.tx.WithTx(ctx, func(ctx context.Context) error { + list, err = c.publicKeyStore.List(ctx, user.ID, filter) + if err != nil { + return fmt.Errorf("failed to list public keys for user: %w", err) + } + + if filter.Page == 1 && len(list) < filter.Size { + count = len(list) + return nil + } + + count, err = c.publicKeyStore.Count(ctx, user.ID, filter) + if err != nil { + return fmt.Errorf("failed to count public keys for user: %w", err) + } + + return nil + }, dbtx.TxDefaultReadOnly) + if err != nil { + return nil, 0, err + } + + return list, count, nil +} diff --git a/app/api/controller/user/wire.go b/app/api/controller/user/wire.go index e01ab6ba3..b45d739b5 100644 --- a/app/api/controller/user/wire.go +++ b/app/api/controller/user/wire.go @@ -35,6 +35,7 @@ func ProvideController( principalStore store.PrincipalStore, tokenStore store.TokenStore, membershipStore store.MembershipStore, + publicKeyStore store.PublicKeyStore, ) *Controller { return NewController( tx, @@ -42,5 +43,6 @@ func ProvideController( authorizer, principalStore, tokenStore, - membershipStore) + membershipStore, + publicKeyStore) } diff --git a/app/api/handler/user/publickey_create.go b/app/api/handler/user/publickey_create.go new file mode 100644 index 000000000..79a9484f9 --- /dev/null +++ b/app/api/handler/user/publickey_create.go @@ -0,0 +1,47 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package user + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/user" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleCreatePublicKey(userCtrl *user.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + userUID := session.Principal.UID + + in := new(user.CreatePublicKeyInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) + return + } + + key, err := userCtrl.CreatePublicKey(ctx, session, userUID, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusCreated, key) + } +} diff --git a/app/api/handler/user/publickey_delete.go b/app/api/handler/user/publickey_delete.go new file mode 100644 index 000000000..4febcaecc --- /dev/null +++ b/app/api/handler/user/publickey_delete.go @@ -0,0 +1,45 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package user + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/user" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleDeletePublicKey(userCtrl *user.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + userUID := session.Principal.UID + + id, err := request.GetPublicKeyIdentifierFromPath(r) + if err != nil { + render.BadRequest(ctx, w) + return + } + + err = userCtrl.DeletePublicKey(ctx, session, userUID, id) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/app/api/handler/user/publickey_list.go b/app/api/handler/user/publickey_list.go new file mode 100644 index 000000000..c8890e1f0 --- /dev/null +++ b/app/api/handler/user/publickey_list.go @@ -0,0 +1,46 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package user + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/user" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleListPublicKeys(userCtrl *user.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + userUID := session.Principal.UID + + filter, err := request.ParseListPublicKeyQueryFilterFromRequest(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + keys, count, err := userCtrl.ListPublicKeys(ctx, session, userUID, &filter) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.Pagination(r, w, filter.Page, filter.Size, count) + render.JSON(w, http.StatusOK, keys) + } +} diff --git a/app/api/openapi/user.go b/app/api/openapi/user.go index fc72e43cb..8e7423dab 100644 --- a/app/api/openapi/user.go +++ b/app/api/openapi/user.go @@ -61,6 +61,36 @@ var queryParameterSortMembershipSpaces = openapi3.ParameterOrRef{ }, } +var queryParameterQueryPublicKey = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamQuery, + In: openapi3.ParameterInQuery, + Description: ptr.String("The substring which is used to filter the public keys by their path identifier."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeString), + }, + }, + }, +} + +var queryParameterSortPublicKey = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamSort, + In: openapi3.ParameterInQuery, + Description: ptr.String("The data by which the public keys are sorted."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeString), + Default: ptrptr(enum.PublicKeySortCreated), + Enum: enum.PublicKeySort("").Enum(), + }, + }, + }, +} + // helper function that constructs the openapi specification // for user account resources. func buildUser(reflector *openapi3.Reflector) { @@ -99,4 +129,35 @@ func buildUser(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opMemberSpaces, new([]types.MembershipSpace), http.StatusOK) _ = reflector.SetJSONResponse(&opMemberSpaces, new(usererror.Error), http.StatusInternalServerError) _ = reflector.Spec.AddOperation(http.MethodGet, "/user/memberships", opMemberSpaces) + + opKeyCreate := openapi3.Operation{} + opKeyCreate.WithTags("user") + opKeyCreate.WithMapOfAnything(map[string]interface{}{"operationId": "createPublicKey"}) + _ = reflector.SetRequest(&opKeyCreate, new(user.CreatePublicKeyInput), http.MethodPost) + _ = reflector.SetJSONResponse(&opKeyCreate, new(types.PublicKey), http.StatusCreated) + _ = reflector.SetJSONResponse(&opKeyCreate, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opKeyCreate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.Spec.AddOperation(http.MethodPost, "/user/keys", opKeyCreate) + + opKeyDelete := openapi3.Operation{} + opKeyDelete.WithTags("user") + opKeyDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deletePublicKey"}) + _ = reflector.SetRequest(&opKeyDelete, struct { + ID string `path:"public_key_identifier"` + }{}, http.MethodDelete) + _ = reflector.SetJSONResponse(&opKeyDelete, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opKeyDelete, new(usererror.Error), http.StatusNotFound) + _ = reflector.SetJSONResponse(&opKeyDelete, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.Spec.AddOperation(http.MethodDelete, "/user/keys/{public_key_identifier}", opKeyDelete) + + opKeyList := openapi3.Operation{} + opKeyList.WithTags("user") + opKeyList.WithMapOfAnything(map[string]interface{}{"operationId": "listPublicKey"}) + opKeyList.WithParameters(queryParameterPage, queryParameterLimit, + queryParameterQueryPublicKey, queryParameterSortPublicKey, queryParameterOrder) + _ = reflector.SetRequest(&opKeyList, struct{}{}, http.MethodGet) + _ = reflector.SetJSONResponse(&opKeyList, new([]types.PublicKey), http.StatusOK) + _ = reflector.SetJSONResponse(&opKeyList, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opKeyList, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.Spec.AddOperation(http.MethodGet, "/user/keys", opKeyList) } diff --git a/app/api/request/publickey.go b/app/api/request/publickey.go new file mode 100644 index 000000000..9d867b772 --- /dev/null +++ b/app/api/request/publickey.go @@ -0,0 +1,53 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package request + +import ( + "net/http" + "net/url" + + "github.com/harness/gitness/app/api/usererror" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +const ( + PathParamPublicKeyIdentifier = "public_key_identifier" +) + +func GetPublicKeyIdentifierFromPath(r *http.Request) (string, error) { + identifier, err := PathParamOrError(r, PathParamPublicKeyIdentifier) + if err != nil { + return "", err + } + + // paths are unescaped + return url.PathUnescape(identifier) +} + +// ParseListPublicKeyQueryFilterFromRequest parses query filter for public keys from the url. +func ParseListPublicKeyQueryFilterFromRequest(r *http.Request) (types.PublicKeyFilter, error) { + sort := enum.PublicKeySort(ParseSort(r)) + sort, ok := sort.Sanitize() + if !ok { + return types.PublicKeyFilter{}, usererror.BadRequest("Invalid value for the sort query parameter.") + } + + return types.PublicKeyFilter{ + ListQueryFilter: ParseListQueryFilterFromRequest(r), + Sort: sort, + Order: ParseOrder(r), + }, nil +} diff --git a/app/router/api.go b/app/router/api.go index f6ba0993c..3a1d186e4 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -628,6 +628,14 @@ func setupUser(r chi.Router, userCtrl *user.Controller) { r.Delete("/", handleruser.HandleDeleteToken(userCtrl, enum.TokenTypeSession)) }) }) + + // Private keys + r.Route("/keys", func(r chi.Router) { + r.Get("/", handleruser.HandleListPublicKeys(userCtrl)) + r.Post("/", handleruser.HandleCreatePublicKey(userCtrl)) + r.Delete(fmt.Sprintf("/{%s}", request.PathParamPublicKeyIdentifier), + handleruser.HandleDeletePublicKey(userCtrl)) + }) }) } diff --git a/app/services/publickey/parse.go b/app/services/publickey/parse.go new file mode 100644 index 000000000..9ad965892 --- /dev/null +++ b/app/services/publickey/parse.go @@ -0,0 +1,100 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package publickey + +import ( + "crypto/sha256" + "encoding/base64" + + "github.com/harness/gitness/errors" + + "github.com/gliderlabs/ssh" + gossh "golang.org/x/crypto/ssh" + "golang.org/x/exp/slices" +) + +var AllowedTypes = []string{ + gossh.KeyAlgoRSA, + gossh.KeyAlgoECDSA256, + gossh.KeyAlgoECDSA384, + gossh.KeyAlgoECDSA521, + gossh.KeyAlgoED25519, + gossh.KeyAlgoSKECDSA256, + gossh.KeyAlgoSKED25519, +} + +var DisallowedTypes = []string{ + gossh.KeyAlgoDSA, +} + +func From(key gossh.PublicKey) KeyInfo { + return KeyInfo{ + Key: key, + } +} + +func ParseString(keyData string) (KeyInfo, string, error) { + return Parse([]byte(keyData)) +} + +func Parse(keyData []byte) (KeyInfo, string, error) { + publicKey, comment, _, _, err := gossh.ParseAuthorizedKey(keyData) + if err != nil { + return KeyInfo{}, "", err + } + + keyType := publicKey.Type() + + // explicitly disallowed + if slices.Contains(DisallowedTypes, keyType) { + return KeyInfo{}, "", errors.InvalidArgument("keys of type %s are not allowed", keyType) + } + + // only allowed + if !slices.Contains(AllowedTypes, keyType) { + return KeyInfo{}, "", errors.InvalidArgument("allowed key types are %v", AllowedTypes) + } + + return KeyInfo{ + Key: publicKey, + }, comment, nil +} + +type KeyInfo struct { + Key gossh.PublicKey +} + +func (key KeyInfo) Matches(s string) bool { + otherKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(s)) + if err != nil { + return false + } + + return key.MatchesKey(otherKey) +} + +func (key KeyInfo) MatchesKey(otherKey gossh.PublicKey) bool { + return ssh.KeysEqual(key.Key, otherKey) +} + +func (key KeyInfo) Fingerprint() string { + sum := sha256.New() + sum.Write(key.Key.Marshal()) + return base64.StdEncoding.EncodeToString(sum.Sum(nil)) +} + +func (key KeyInfo) Type() string { + return key.Key.Type() +} diff --git a/app/services/publickey/publickey.go b/app/services/publickey/publickey.go new file mode 100644 index 000000000..2213c21fe --- /dev/null +++ b/app/services/publickey/publickey.go @@ -0,0 +1,91 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package publickey + +import ( + "context" + "fmt" + "time" + + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/errors" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + + "github.com/gliderlabs/ssh" +) + +type Service interface { + ValidateKey(ctx context.Context, publicKey ssh.PublicKey, usage enum.PublicKeyUsage) (*types.PrincipalInfo, error) +} + +func NewService( + publicKeyStore store.PublicKeyStore, + pCache store.PrincipalInfoCache, +) LocalService { + return LocalService{ + publicKeyStore: publicKeyStore, + pCache: pCache, + } +} + +type LocalService struct { + publicKeyStore store.PublicKeyStore + pCache store.PrincipalInfoCache +} + +// ValidateKey tries to match the provided key to one of the keys in the database. +// It updates the verified timestamp of the matched key to mark it as used. +func (s LocalService) ValidateKey( + ctx context.Context, + publicKey ssh.PublicKey, + usage enum.PublicKeyUsage, +) (*types.PrincipalInfo, error) { + key := From(publicKey) + fingerprint := key.Fingerprint() + + existingKeys, err := s.publicKeyStore.ListByFingerprint(ctx, fingerprint) + if err != nil { + return nil, fmt.Errorf("failed to read keys by fingerprint: %w", err) + } + + var keyID int64 + var principalID int64 + + for _, existingKey := range existingKeys { + if !key.Matches(existingKey.Content) || existingKey.Usage != usage { + continue + } + + keyID = existingKey.ID + principalID = existingKey.PrincipalID + } + + if keyID == 0 { + return nil, errors.NotFound("Unrecognized key") + } + + pInfo, err := s.pCache.Get(ctx, principalID) + if err != nil { + return nil, fmt.Errorf("failed to pull principal info by public key's principal ID: %w", err) + } + + err = s.publicKeyStore.MarkAsVerified(ctx, keyID, time.Now().UnixMilli()) + if err != nil { + return nil, fmt.Errorf("failed mark key as verified: %w", err) + } + + return pInfo, nil +} diff --git a/app/services/publickey/wire.go b/app/services/publickey/wire.go new file mode 100644 index 000000000..1bcce158d --- /dev/null +++ b/app/services/publickey/wire.go @@ -0,0 +1,32 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package publickey + +import ( + "github.com/harness/gitness/app/store" + + "github.com/google/wire" +) + +var WireSet = wire.NewSet( + ProvidePublicKey, +) + +func ProvidePublicKey( + publicKeyStore store.PublicKeyStore, + pCache store.PrincipalInfoCache, +) Service { + return NewService(publicKeyStore, pCache) +} diff --git a/app/store/database.go b/app/store/database.go index 14b0ccd0f..ec590e23e 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -806,4 +806,30 @@ type ( // FindByIdentifier returns a types.UserGroup given a space ID and identifier. FindByIdentifier(ctx context.Context, spaceID int64, identifier string) (*types.UserGroup, error) } + + PublicKeyStore interface { + // Find returns a public key given an ID. + Find(ctx context.Context, id int64) (*types.PublicKey, error) + + // FindByIdentifier returns a public key given a principal ID and an identifier. + FindByIdentifier(ctx context.Context, principalID int64, identifier string) (*types.PublicKey, error) + + // Create creates a new public key. + Create(ctx context.Context, publicKey *types.PublicKey) error + + // DeleteByIdentifier deletes a public key. + DeleteByIdentifier(ctx context.Context, principalID int64, identifier string) error + + // MarkAsVerified updates the public key to mark it as verified. + MarkAsVerified(ctx context.Context, id int64, verified int64) error + + // Count returns the number of public keys for the principal that match provided the filter. + Count(ctx context.Context, principalID int64, filter *types.PublicKeyFilter) (int, error) + + // List returns the public keys for the principal that match provided the filter. + List(ctx context.Context, principalID int64, filter *types.PublicKeyFilter) ([]types.PublicKey, error) + + // ListByFingerprint returns public keys given a fingerprint and key usage. + ListByFingerprint(ctx context.Context, fingerprint string) ([]types.PublicKey, error) + } ) diff --git a/app/store/database/migrate/postgres/0051_create_table_public_key.down.sql b/app/store/database/migrate/postgres/0051_create_table_public_key.down.sql new file mode 100644 index 000000000..e2684bfdc --- /dev/null +++ b/app/store/database/migrate/postgres/0051_create_table_public_key.down.sql @@ -0,0 +1,3 @@ +DROP INDEX public_keys_usage_fingerprint; +DROP INDEX public_keys_principal_id; +DROP TABLE public_keys; \ No newline at end of file diff --git a/app/store/database/migrate/postgres/0051_create_table_public_key.up.sql b/app/store/database/migrate/postgres/0051_create_table_public_key.up.sql new file mode 100644 index 000000000..54815a00e --- /dev/null +++ b/app/store/database/migrate/postgres/0051_create_table_public_key.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE public_keys ( + public_key_id SERIAL PRIMARY KEY +,public_key_principal_id INTEGER NOT NULL +,public_key_created BIGINT NOT NULL +,public_key_verified BIGINT +,public_key_identifier TEXT NOT NULL +,public_key_usage TEXT NOT NULL +,public_key_fingerprint TEXT NOT NULL +,public_key_content TEXT NOT NULL +,public_key_comment TEXT NOT NULL +,public_key_type TEXT NOT NULL +,CONSTRAINT fk_public_key_principal_id FOREIGN KEY (public_key_principal_id) + REFERENCES principals (principal_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); + +CREATE INDEX public_keys_fingerprint + ON public_keys(public_key_fingerprint); + +CREATE UNIQUE INDEX public_keys_principal_id_identifier + ON public_keys(public_key_principal_id, LOWER(public_key_identifier)); diff --git a/app/store/database/migrate/sqlite/0051_create_table_public_key.down.sql b/app/store/database/migrate/sqlite/0051_create_table_public_key.down.sql new file mode 100644 index 000000000..e2684bfdc --- /dev/null +++ b/app/store/database/migrate/sqlite/0051_create_table_public_key.down.sql @@ -0,0 +1,3 @@ +DROP INDEX public_keys_usage_fingerprint; +DROP INDEX public_keys_principal_id; +DROP TABLE public_keys; \ No newline at end of file diff --git a/app/store/database/migrate/sqlite/0051_create_table_public_key.up.sql b/app/store/database/migrate/sqlite/0051_create_table_public_key.up.sql new file mode 100644 index 000000000..4b0108ce8 --- /dev/null +++ b/app/store/database/migrate/sqlite/0051_create_table_public_key.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE public_keys ( + public_key_id INTEGER PRIMARY KEY AUTOINCREMENT +,public_key_principal_id INTEGER NOT NULL +,public_key_created BIGINT NOT NULL +,public_key_verified BIGINT +,public_key_identifier TEXT NOT NULL +,public_key_usage TEXT NOT NULL +,public_key_fingerprint TEXT NOT NULL +,public_key_content TEXT NOT NULL +,public_key_comment TEXT NOT NULL +,public_key_type TEXT NOT NULL +,CONSTRAINT fk_public_key_principal_id FOREIGN KEY (public_key_principal_id) + REFERENCES principals (principal_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); + +CREATE INDEX public_keys_fingerprint + ON public_keys(public_key_fingerprint); + +CREATE UNIQUE INDEX public_keys_principal_id_identifier + ON public_keys(public_key_principal_id, LOWER(public_key_identifier)); diff --git a/app/store/database/publickey.go b/app/store/database/publickey.go new file mode 100644 index 000000000..6c38f1eb1 --- /dev/null +++ b/app/store/database/publickey.go @@ -0,0 +1,356 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package database + +import ( + "context" + "fmt" + "strings" + + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/errors" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + + "github.com/Masterminds/squirrel" + "github.com/guregu/null" + "github.com/jmoiron/sqlx" +) + +var _ store.PublicKeyStore = PublicKeyStore{} + +// NewPublicKeyStore returns a new PublicKeyStore. +func NewPublicKeyStore(db *sqlx.DB) PublicKeyStore { + return PublicKeyStore{ + db: db, + } +} + +// PublicKeyStore implements a store.PublicKeyStore backed by a relational database. +type PublicKeyStore struct { + db *sqlx.DB +} + +type publicKey struct { + ID int64 `db:"public_key_id"` + + PrincipalID int64 `db:"public_key_principal_id"` + + Created int64 `db:"public_key_created"` + Verified null.Int `db:"public_key_verified"` + + Identifier string `db:"public_key_identifier"` + Usage string `db:"public_key_usage"` + + Fingerprint string `db:"public_key_fingerprint"` + Content string `db:"public_key_content"` + Comment string `db:"public_key_comment"` + Type string `db:"public_key_type"` +} + +const ( + publicKeyColumns = ` + public_key_id + ,public_key_principal_id + ,public_key_created + ,public_key_verified + ,public_key_identifier + ,public_key_usage + ,public_key_fingerprint + ,public_key_content + ,public_key_comment + ,public_key_type` + + publicKeySelectBase = ` + SELECT` + publicKeyColumns + ` + FROM public_keys` +) + +// Find fetches a job by its unique identifier. +func (s PublicKeyStore) Find(ctx context.Context, id int64) (*types.PublicKey, error) { + const sqlQuery = publicKeySelectBase + ` + WHERE public_key_id = $1` + + db := dbtx.GetAccessor(ctx, s.db) + + result := &publicKey{} + if err := db.GetContext(ctx, result, sqlQuery, id); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find public key by id") + } + + key := mapToPublicKey(result) + + return &key, nil +} + +// FindByIdentifier returns a public key given a principal ID and an identifier. +func (s PublicKeyStore) FindByIdentifier( + ctx context.Context, + principalID int64, + identifier string, +) (*types.PublicKey, error) { + const sqlQuery = publicKeySelectBase + ` + WHERE public_key_principal_id = $1 and LOWER(public_key_identifier) = $2` + + db := dbtx.GetAccessor(ctx, s.db) + + result := &publicKey{} + if err := db.GetContext(ctx, result, sqlQuery, principalID, strings.ToLower(identifier)); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find public key by principal and identifier") + } + + key := mapToPublicKey(result) + + return &key, nil +} + +// Create creates a new public key. +func (s PublicKeyStore) Create(ctx context.Context, key *types.PublicKey) error { + const sqlQuery = ` + INSERT INTO public_keys ( + public_key_principal_id + ,public_key_created + ,public_key_verified + ,public_key_identifier + ,public_key_usage + ,public_key_fingerprint + ,public_key_content + ,public_key_comment + ,public_key_type + ) values ( + :public_key_principal_id + ,:public_key_created + ,:public_key_verified + ,:public_key_identifier + ,:public_key_usage + ,:public_key_fingerprint + ,:public_key_content + ,:public_key_comment + ,:public_key_type + ) RETURNING public_key_id` + + db := dbtx.GetAccessor(ctx, s.db) + + dbKey := mapToInternalPublicKey(key) + + query, arg, err := db.BindNamed(sqlQuery, &dbKey) + if err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to bind public key object") + } + + if err = db.QueryRowContext(ctx, query, arg...).Scan(&dbKey.ID); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Insert public key query failed") + } + + key.ID = dbKey.ID + + return nil +} + +// DeleteByIdentifier deletes a public key. +func (s PublicKeyStore) DeleteByIdentifier(ctx context.Context, principalID int64, identifier string) error { + const sqlQuery = `DELETE FROM public_keys WHERE public_key_principal_id = $1 and LOWER(public_key_identifier) = $2` + + db := dbtx.GetAccessor(ctx, s.db) + + result, err := db.ExecContext(ctx, sqlQuery, principalID, strings.ToLower(identifier)) + if err != nil { + return database.ProcessSQLErrorf(ctx, err, "Delete public key query failed") + } + + count, err := result.RowsAffected() + if err != nil { + return database.ProcessSQLErrorf(ctx, err, "RowsAffected after delete of public key failed") + } + + if count == 0 { + return errors.NotFound("Key not found") + } + + return nil +} + +// MarkAsVerified updates the public key to mark it as verified. +func (s PublicKeyStore) MarkAsVerified(ctx context.Context, id int64, verified int64) error { + const sqlQuery = ` + UPDATE public_keys + SET public_key_verified = $1 + WHERE public_key_id = $2` + + if _, err := dbtx.GetAccessor(ctx, s.db).ExecContext(ctx, sqlQuery, verified, id); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to mark public key as varified") + } + + return nil +} + +func (s PublicKeyStore) Count( + ctx context.Context, + principalID int64, + filter *types.PublicKeyFilter, +) (int, error) { + stmt := database.Builder. + Select("count(*)"). + From("public_keys"). + Where("public_key_principal_id = ?", principalID) + + stmt = s.applyQueryFilter(stmt, filter) + + sql, args, err := stmt.ToSql() + if err != nil { + return 0, fmt.Errorf("failed to convert query to sql: %w", err) + } + + db := dbtx.GetAccessor(ctx, s.db) + + var count int + + if err := db.QueryRowContext(ctx, sql, args...).Scan(&count); err != nil { + return 0, database.ProcessSQLErrorf(ctx, err, "failed to execute count public keys query") + } + + return count, nil +} + +// List returns the public keys for the principal. +func (s PublicKeyStore) List( + ctx context.Context, + principalID int64, + filter *types.PublicKeyFilter, +) ([]types.PublicKey, error) { + stmt := database.Builder. + Select(publicKeyColumns). + From("public_keys"). + Where("public_key_principal_id = ?", principalID) + + stmt = s.applyQueryFilter(stmt, filter) + stmt = s.applySortFilter(stmt, filter) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, fmt.Errorf("failed to convert query to sql: %w", err) + } + + db := dbtx.GetAccessor(ctx, s.db) + + keys := make([]publicKey, 0) + if err = db.SelectContext(ctx, &keys, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "failed to execute list public keys query") + } + + return mapToPublicKeys(keys), nil +} + +// ListByFingerprint returns public keys given a fingerprint and key usage. +func (s PublicKeyStore) ListByFingerprint( + ctx context.Context, + fingerprint string, +) ([]types.PublicKey, error) { + stmt := database.Builder. + Select(publicKeyColumns). + From("public_keys"). + Where("public_key_fingerprint = ?", fingerprint). + OrderBy("public_key_created ASC") + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, fmt.Errorf("failed to convert query to sql: %w", err) + } + + db := dbtx.GetAccessor(ctx, s.db) + + keys := make([]publicKey, 0) + if err = db.SelectContext(ctx, &keys, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "failed to execute public keys by fingerprint query") + } + + return mapToPublicKeys(keys), nil +} + +func (PublicKeyStore) applyQueryFilter( + stmt squirrel.SelectBuilder, + filter *types.PublicKeyFilter, +) squirrel.SelectBuilder { + if filter.Query != "" { + stmt = stmt.Where("LOWER(public_key_identifier) LIKE ?", + fmt.Sprintf("%%%s%%", strings.ToLower(filter.Query))) + } + + return stmt +} + +func (PublicKeyStore) applySortFilter( + stmt squirrel.SelectBuilder, + filter *types.PublicKeyFilter, +) squirrel.SelectBuilder { + stmt = stmt.Limit(database.Limit(filter.Size)) + stmt = stmt.Offset(database.Offset(filter.Page, filter.Size)) + + order := filter.Order + if order == enum.OrderDefault { + order = enum.OrderAsc + } + + switch filter.Sort { + case enum.PublicKeySortIdentifier: + stmt = stmt.OrderBy("public_key_identifier " + order.String()) + case enum.PublicKeySortCreated: + stmt = stmt.OrderBy("public_key_created " + order.String()) + } + + return stmt +} + +func mapToInternalPublicKey(in *types.PublicKey) publicKey { + return publicKey{ + ID: in.ID, + PrincipalID: in.PrincipalID, + Created: in.Created, + Verified: null.IntFromPtr(in.Verified), + Identifier: in.Identifier, + Usage: string(in.Usage), + Fingerprint: in.Fingerprint, + Content: in.Content, + Comment: in.Comment, + Type: in.Type, + } +} + +func mapToPublicKey(in *publicKey) types.PublicKey { + return types.PublicKey{ + ID: in.ID, + PrincipalID: in.PrincipalID, + Created: in.Created, + Verified: in.Verified.Ptr(), + Identifier: in.Identifier, + Usage: enum.PublicKeyUsage(in.Usage), + Fingerprint: in.Fingerprint, + Content: in.Content, + Comment: in.Comment, + Type: in.Type, + } +} + +func mapToPublicKeys( + keys []publicKey, +) []types.PublicKey { + res := make([]types.PublicKey, len(keys)) + for i := 0; i < len(keys); i++ { + res[i] = mapToPublicKey(&keys[i]) + } + return res +} diff --git a/app/store/database/wire.go b/app/store/database/wire.go index e3f06e461..d1c005438 100644 --- a/app/store/database/wire.go +++ b/app/store/database/wire.go @@ -59,6 +59,7 @@ var WireSet = wire.NewSet( ProvideTemplateStore, ProvideTriggerStore, ProvidePluginStore, + ProvidePublicKeyStore, ) // migrator is helper function to set up the database by performing automated @@ -253,3 +254,8 @@ func ProvideSettingsStore(db *sqlx.DB) store.SettingsStore { func ProvidePublicAccessStore(db *sqlx.DB) store.PublicAccessStore { return NewPublicAccessStore(db) } + +// ProvidePublicKeyStore provides a public key store. +func ProvidePublicKeyStore(db *sqlx.DB) store.PublicKeyStore { + return NewPublicKeyStore(db) +} diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index bf2aeef33..bdb38ab66 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -122,7 +122,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro principalUIDTransformation := store.ProvidePrincipalUIDTransformation() principalStore := database.ProvidePrincipalStore(db, principalUIDTransformation) tokenStore := database.ProvideTokenStore(db) - controller := user.ProvideController(transactor, principalUID, authorizer, principalStore, tokenStore, membershipStore) + publicKeyStore := database.ProvidePublicKeyStore(db) + controller := user.ProvideController(transactor, principalUID, authorizer, principalStore, tokenStore, membershipStore, publicKeyStore) serviceController := service.NewController(principalUID, authorizer, principalStore) bootstrapBootstrap := bootstrap.ProvideBootstrap(config, controller, serviceController) authenticator := authn.ProvideAuthenticator(config, principalStore, tokenStore) diff --git a/go.mod b/go.mod index 8becdd055..983962221 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/drone/spec v0.0.0-20230919004456-7455b8913ff5 github.com/fatih/color v1.16.0 github.com/gabriel-vasile/mimetype v1.4.3 + github.com/gliderlabs/ssh v0.3.7 github.com/go-chi/chi v1.5.4 github.com/go-chi/cors v1.2.1 github.com/go-redis/redis/v8 v8.11.5 @@ -57,12 +58,12 @@ require ( github.com/zricethezav/gitleaks/v8 v8.18.2 go.starlark.net v0.0.0-20231121155337-90ade8b19d09 go.uber.org/multierr v1.8.0 - golang.org/x/crypto v0.14.0 + golang.org/x/crypto v0.17.0 golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a golang.org/x/oauth2 v0.10.0 golang.org/x/sync v0.3.0 - golang.org/x/term v0.13.0 - golang.org/x/text v0.13.0 + golang.org/x/term v0.15.0 + golang.org/x/text v0.14.0 google.golang.org/api v0.132.0 gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/mail.v2 v2.3.1 @@ -76,6 +77,7 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e // indirect github.com/BobuSumisu/aho-corasick v1.0.3 // indirect + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/antonmedv/expr v1.15.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -165,14 +167,14 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect - github.com/swaggest/jsonschema-go v0.3.40 // indirect + github.com/swaggest/jsonschema-go v0.3.40 github.com/swaggest/refl v1.1.0 // indirect github.com/vearutop/statigz v1.4.0 // indirect github.com/yuin/goldmark v1.4.13 go.uber.org/atomic v1.10.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/tools v0.13.0 // indirect google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 7610b8a59..ae76f4794 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antonmedv/expr v1.15.2 h1:afFXpDWIC2n3bF+kTZE1JvFo+c34uaM3sTqh8z0xfdU= github.com/antonmedv/expr v1.15.2/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= @@ -194,10 +196,6 @@ github.com/drone/go-convert v0.0.0-20230919093251-7104c3bcc635 h1:qQX+U2iEm4X2Fc github.com/drone/go-convert v0.0.0-20230919093251-7104c3bcc635/go.mod h1:PyCDcuAhGF6W0VJ6qMmlM47dsSyGv/zDiMqeJxMFuGM= github.com/drone/go-generate v0.0.0-20230920014042-6085ee5c9522 h1:i3EfRpr/eYifK9w0ninT3xHAthkS4NTQjLX0/zDIsy4= github.com/drone/go-generate v0.0.0-20230920014042-6085ee5c9522/go.mod h1:eTfy716efMJgVvk/ZkRvitaXY2UuytfqDjxclFMeLdQ= -github.com/drone/go-scm v1.31.2 h1:6hZxf0aETV17830fMCPrgcA4y8j/8Gdfy0xEdInUeqQ= -github.com/drone/go-scm v1.31.2/go.mod h1:DFIJJjhMj0TSXPz+0ni4nyZ9gtTtC40Vh/TGRugtyWw= -github.com/drone/go-scm v1.36.0 h1:5d9lJVoXGJvLqG2CFwLcZvMMiZPNxCrmHbxRVzu19qM= -github.com/drone/go-scm v1.36.0/go.mod h1:DFIJJjhMj0TSXPz+0ni4nyZ9gtTtC40Vh/TGRugtyWw= github.com/drone/go-scm v1.37.1 h1:U42+3JRFvmBJXZnKqphF377h2mT9Pe+Oin/lDD1fnJ8= github.com/drone/go-scm v1.37.1/go.mod h1:DFIJJjhMj0TSXPz+0ni4nyZ9gtTtC40Vh/TGRugtyWw= github.com/drone/runner-go v1.12.0 h1:zUjDj9ylsJ4n4Mvy4znddq/Z4EBzcUXzTltpzokKtgs= @@ -236,6 +234,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gitleaks/go-gitdiff v0.9.0 h1:SHAU2l0ZBEo8g82EeFewhVy81sb7JCxW76oSPtR/Nqg= github.com/gitleaks/go-gitdiff v0.9.0/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= @@ -249,7 +249,6 @@ github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgO github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= @@ -299,7 +298,6 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -509,19 +507,16 @@ github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXL github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -608,7 +603,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= @@ -616,7 +610,6 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4/go.mod h1:cojhOHk1gbMeklOyDP2oKKLftefXoJreOQGOrXk+Z38= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= @@ -878,8 +871,8 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1078,14 +1071,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1097,8 +1090,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/types/enum/public_key.go b/types/enum/public_key.go new file mode 100644 index 000000000..7e52dce93 --- /dev/null +++ b/types/enum/public_key.go @@ -0,0 +1,56 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package enum + +// PublicKeyUsage represents usage type of public key. +type PublicKeyUsage string + +// PublicKeyUsage enumeration. +const ( + PublicKeyUsageAuth PublicKeyUsage = "auth" + PublicKeyUsageSign PublicKeyUsage = "sign" +) + +var publicKeyTypes = sortEnum([]PublicKeyUsage{ + PublicKeyUsageAuth, +}) + +func (PublicKeyUsage) Enum() []interface{} { return toInterfaceSlice(publicKeyTypes) } +func (s PublicKeyUsage) Sanitize() (PublicKeyUsage, bool) { + return Sanitize(s, GetAllPublicKeyUsages) +} +func GetAllPublicKeyUsages() ([]PublicKeyUsage, PublicKeyUsage) { + return publicKeyTypes, PublicKeyUsageAuth +} + +// PublicKeySort is used to specify sorting of public keys. +type PublicKeySort string + +// PublicKeySort enumeration. +const ( + PublicKeySortCreated PublicKeySort = "created" + PublicKeySortIdentifier PublicKeySort = "identifier" +) + +var publicKeySorts = sortEnum([]PublicKeySort{ + PublicKeySortCreated, + PublicKeySortIdentifier, +}) + +func (PublicKeySort) Enum() []interface{} { return toInterfaceSlice(publicKeySorts) } +func (s PublicKeySort) Sanitize() (PublicKeySort, bool) { return Sanitize(s, GetAllPublicKeySorts) } +func GetAllPublicKeySorts() ([]PublicKeySort, PublicKeySort) { + return publicKeySorts, PublicKeySortCreated +} diff --git a/types/public_key.go b/types/public_key.go new file mode 100644 index 000000000..7c2ce295e --- /dev/null +++ b/types/public_key.go @@ -0,0 +1,36 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import "github.com/harness/gitness/types/enum" + +type PublicKey struct { + ID int64 `json:"-"` // frontend doesn't need it + PrincipalID int64 `json:"-"` // API always returns keys for the same user + Created int64 `json:"created"` + Verified *int64 `json:"verified"` + Identifier string `json:"identifier"` + Usage enum.PublicKeyUsage `json:"usage"` + Fingerprint string `json:"fingerprint"` + Content string `json:"-"` + Comment string `json:"comment"` + Type string `json:"type"` +} + +type PublicKeyFilter struct { + ListQueryFilter + Sort enum.PublicKeySort + Order enum.Order +}