Skip to content

Commit

Permalink
feature: add ssh key add operations to profile api
Browse files Browse the repository at this point in the history
  • Loading branch information
greenpau committed Mar 19, 2024
1 parent a3a4d0e commit 7bdfa68
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 33 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ qtest: covdir
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewJwksKeyFromRSAPublicKeyPEM ./pkg/idp/oauth/*.go
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewIdentityProviderConfig ./pkg/idp/*.go
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authn/ui/...
@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/ids/...
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/ids/...
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/ids/local/*.go
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/ids/ldap/*.go
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authz/...
Expand All @@ -152,7 +152,7 @@ qtest: covdir
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authproxy/...
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/identity/...
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authn/backends/...
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run NewAPIKey ./pkg/identity/...
@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run NewPublicKey ./pkg/identity/...
@#
@go tool cover -html=.coverage/coverage.out -o .coverage/coverage.html;
@#go tool cover -func=.coverage/coverage.out | grep -v "100.0"
Expand Down
118 changes: 118 additions & 0 deletions pkg/authn/api_add_user_ssh_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2024 Paul Greenberg [email protected]
//
// 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 authn

import (
"context"
"fmt"
"net/http"
"regexp"
"strings"

"github.com/greenpau/go-authcrunch/pkg/authn/enums/operator"
"github.com/greenpau/go-authcrunch/pkg/ids"
"github.com/greenpau/go-authcrunch/pkg/requests"
"github.com/greenpau/go-authcrunch/pkg/tagging"
"github.com/greenpau/go-authcrunch/pkg/user"
)

var sshKeyTitleRegexPattern = regexp.MustCompile(`^[\w\@\.\s\(\)]+$`)

// AddUserSSHKey adds SSH key to user identity.
func (p *Portal) AddUserSSHKey(
ctx context.Context,
w http.ResponseWriter,
r *http.Request,
rr *requests.Request,
parsedUser *user.User,
resp map[string]interface{},
usr *user.User,
backend ids.IdentityStore,
bodyData map[string]interface{}) error {

var keyTitle, keyDescription, keyPayload string
var keyLabels []string = []string{}
var keyTags []tagging.Tag = []tagging.Tag{}

// Extract data.
if v, exists := bodyData["content"]; exists {
switch exp := v.(type) {
case string:
keyPayload = strings.TrimSpace(exp)
default:
resp["message"] = "Profile API did find key content in the request payload, but it is malformed"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}
} else {
resp["message"] = "Profile API did not find key content in the request payload"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}
if v, exists := bodyData["title"]; exists {
keyTitle = v.(string)
} else {
resp["message"] = "Profile API did not find title in the request payload"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}
if v, exists := bodyData["description"]; exists {
keyDescription = v.(string)
} else {
resp["message"] = "Profile API did not find description in the request payload"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}
if extractedTokenTags, err := tagging.ExtractTags(bodyData); err == nil {
for _, extractedTokenTag := range extractedTokenTags {
keyTags = append(keyTags, *extractedTokenTag)
}
} else {
resp["message"] = "Profile API find malformed tags in the request payload"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}
if extractedTokenLabels, err := tagging.ExtractLabels(bodyData); err == nil {
keyLabels = extractedTokenLabels
} else {
resp["message"] = "Profile API find malformed tags in the request payload"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}

// Validate data.
if !sshKeyTitleRegexPattern.MatchString(keyTitle) {
resp["message"] = "Profile API found non-compliant SSH key title value"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}
if !tokenDescriptionRegexPattern.MatchString(keyDescription) && (keyDescription != "") {
resp["message"] = "Profile API found non-compliant SSH key description value"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}
if !sshKeyRegexPattern1.MatchString(keyPayload) && !sshKeyRegexPattern2.MatchString(keyPayload) {
resp["message"] = "Profile API found non-compliant SSH public key value"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}

rr.Key.Usage = "ssh"
rr.Key.Comment = keyTitle
rr.Key.Description = keyDescription
rr.Key.Payload = keyPayload
rr.Key.Labels = keyLabels
rr.Key.Tags = keyTags

if err := backend.Request(operator.AddKeySSH, rr); err != nil {
var errMsg string = fmt.Sprintf("the Profile API failed to add SSH key to identity store: %v", err)
resp["message"] = errMsg
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}

resp["entry"] = "Created"
return handleAPIProfileResponse(w, rr, http.StatusOK, resp)
}
8 changes: 7 additions & 1 deletion pkg/authn/api_fetch_user_api_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ func (p *Portal) FetchUserAPIKeys(
return handleAPIProfileResponse(w, rr, http.StatusInternalServerError, resp)
}
bundle := rr.Response.Payload.(*identity.APIKeyBundle)
resp["entries"] = bundle.Get()

apiKeys := bundle.Get()
for _, apiKey := range apiKeys {
apiKey.Payload = ""
apiKey.Prefix = ""
}
resp["entries"] = apiKeys
return handleAPIProfileResponse(w, rr, http.StatusOK, resp)
}

Expand Down
7 changes: 6 additions & 1 deletion pkg/authn/api_fetch_user_multi_factor_authenticators.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ func (p *Portal) FetchUserMultiFactorVerifiers(
return handleAPIProfileResponse(w, rr, http.StatusInternalServerError, resp)
}
bundle := rr.Response.Payload.(*identity.MfaTokenBundle)
resp["entries"] = bundle.Get()

tokens := bundle.Get()
for _, token := range tokens {
token.Secret = ""
}
resp["entries"] = tokens
return handleAPIProfileResponse(w, rr, http.StatusOK, resp)
}

Expand Down
8 changes: 7 additions & 1 deletion pkg/authn/api_fetch_user_ssh_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ func (p *Portal) FetchUserSSHKeys(
return handleAPIProfileResponse(w, rr, http.StatusInternalServerError, resp)
}
bundle := rr.Response.Payload.(*identity.PublicKeyBundle)
resp["entries"] = bundle.Get()
sshKeys := bundle.Get()
for _, sshKey := range sshKeys {
sshKey.Payload = ""
sshKey.OpenSSH = ""
}
resp["entries"] = sshKeys

return handleAPIProfileResponse(w, rr, http.StatusOK, resp)
}

Expand Down
90 changes: 90 additions & 0 deletions pkg/authn/api_test_user_ssh_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2024 Paul Greenberg [email protected]
//
// 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 authn

import (
"context"
"fmt"
"net/http"
"regexp"

"github.com/greenpau/go-authcrunch/pkg/identity"
"github.com/greenpau/go-authcrunch/pkg/ids"
"github.com/greenpau/go-authcrunch/pkg/requests"
"github.com/greenpau/go-authcrunch/pkg/user"
)

var sshKeyRegexPattern1 = regexp.MustCompile(`^ssh-[a-z]+\s*[A-z0-9\+\/\=\n]+\s*`)
var sshKeyRegexPattern2 = regexp.MustCompile(`^[-]{3,5}\s*BEGIN\s[A-Z0-9]+\sPUBLIC\sKEY[-]{3,5}\s*`)

// TestUserSSHKey tests SSH key.
func (p *Portal) TestUserSSHKey(
ctx context.Context,
w http.ResponseWriter,
r *http.Request,
rr *requests.Request,
parsedUser *user.User,
resp map[string]interface{},
usr *user.User,
backend ids.IdentityStore,
bodyData map[string]interface{}) error {

rr.Key.Usage = "ssh"

// Extract data.
if v, exists := bodyData["content"]; exists {
switch keyContent := v.(type) {
case string:
rr.Key.Payload = keyContent
default:
resp["message"] = "Profile API did find key content in the request payload, but it is malformed"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}
} else {
resp["message"] = "Profile API did not find key content in the request payload"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}

// Validate data.
if !sshKeyRegexPattern1.MatchString(rr.Key.Payload) && !sshKeyRegexPattern2.MatchString(rr.Key.Payload) {
resp["message"] = "Profile API found non-compliant SSH public key value"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}

pk, err := identity.NewPublicKey(rr)
if err != nil {
var errMsg string = fmt.Sprintf("the Profile API failed to convert input into SSH key: %v", err)
resp["message"] = errMsg
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}

respData := make(map[string]interface{})
if pk != nil {
respData["success"] = true
if pk.FingerprintMD5 != "" {
respData["fingerprint_md5"] = pk.FingerprintMD5
}
if pk.Fingerprint != "" {
respData["fingerprint"] = pk.Fingerprint
}
if pk.Comment != "" {
respData["comment"] = pk.Comment
}
} else {
respData["success"] = false
}
resp["entry"] = respData
return handleAPIProfileResponse(w, rr, http.StatusOK, resp)
}
8 changes: 7 additions & 1 deletion pkg/authn/handle_api_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,13 @@ func (p *Portal) handleAPIProfile(ctx context.Context, w http.ResponseWriter, r
case "fetch_user_ssh_keys":
case "fetch_user_ssh_key":
case "delete_user_ssh_key":
case "test_user_ssh_key":
case "add_user_ssh_key":
case "fetch_user_gpg_keys":
case "fetch_user_gpg_key":
case "delete_user_gpg_key":
default:
resp["message"] = "Profile API recieved unsupported request type"
resp["message"] = "Profile API received unsupported request type"
return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp)
}

Expand Down Expand Up @@ -182,6 +184,10 @@ func (p *Portal) handleAPIProfile(ctx context.Context, w http.ResponseWriter, r
return p.FetchUserSSHKey(ctx, w, r, rr, parsedUser, resp, usr, backend, bodyData)
case "delete_user_ssh_key":
return p.DeleteUserSSHKey(ctx, w, r, rr, parsedUser, resp, usr, backend, bodyData)
case "test_user_ssh_key":
return p.TestUserSSHKey(ctx, w, r, rr, parsedUser, resp, usr, backend, bodyData)
case "add_user_ssh_key":
return p.AddUserSSHKey(ctx, w, r, rr, parsedUser, resp, usr, backend, bodyData)
case "fetch_user_gpg_keys":
return p.FetchUserGPGKeys(ctx, w, r, rr, parsedUser, resp, usr, backend)
case "fetch_user_gpg_key":
Expand Down
5 changes: 4 additions & 1 deletion pkg/identity/public_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,17 +259,20 @@ func (p *PublicKey) parsePublicKeyRSA() error {
if p.Usage != "ssh" {
return errors.ErrPublicKeyUsagePayloadMismatch.WithArgs(p.Usage)
}

block, _ := pem.Decode(bytes.TrimSpace([]byte(p.Payload)))
if block == nil {
return errors.ErrPublicKeyBlockType.WithArgs("")
}
if block.Type != "RSA PUBLIC KEY" {
return errors.ErrPublicKeyBlockType.WithArgs(block.Type)
}
publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes)

publicKeyInterface, err := x509.ParsePKCS1PublicKey(block.Bytes)
if err != nil {
return errors.ErrPublicKeyParse.WithArgs(err)
}

publicKey, err := ssh.NewPublicKey(publicKeyInterface)
if err != nil {
return fmt.Errorf("failed ssh.NewPublicKey: %s", err)
Expand Down
Loading

0 comments on commit 7bdfa68

Please sign in to comment.