mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
[Search-based access requests] Include allowed resource IDs in user certs (#12494)
This commit is contained in:
parent
2fbf94bf50
commit
b55320f806
|
@ -512,6 +512,8 @@ type NewWebSessionRequest struct {
|
|||
LoginTime time.Time
|
||||
// AccessRequests contains the UUIDs of the access requests currently in use.
|
||||
AccessRequests []string
|
||||
// RequestedResourceIDs optionally lists requested resources
|
||||
RequestedResourceIDs []ResourceID
|
||||
}
|
||||
|
||||
// Check validates the request.
|
||||
|
|
|
@ -443,6 +443,9 @@ const (
|
|||
// CertExtensionGeneration counts the number of times a certificate has
|
||||
// been renewed.
|
||||
CertExtensionGeneration = "generation"
|
||||
// CertExtensionAllowedResources lists the resources which this certificate
|
||||
// should be allowed to access
|
||||
CertExtensionAllowedResources = "allowed-resources"
|
||||
)
|
||||
|
||||
// Note: when adding new providers to this list, consider updating the help message for --provider flag
|
||||
|
|
368
lib/auth/access_request_test.go
Normal file
368
lib/auth/access_request_test.go
Normal file
|
@ -0,0 +1,368 @@
|
|||
/*
|
||||
Copyright 2022 Gravitational, 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils/sshutils"
|
||||
"github.com/gravitational/teleport/lib/auth/native"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAccessRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
testAuthServer, err := NewTestAuthServer(TestAuthServerConfig{
|
||||
Dir: t.TempDir(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
server, err := testAuthServer.NewTestTLSServer()
|
||||
require.NoError(t, err)
|
||||
|
||||
roleSpecs := map[string]types.RoleSpecV5{
|
||||
// admins is the role to be requested
|
||||
"admins": types.RoleSpecV5{
|
||||
Allow: types.RoleConditions{
|
||||
Logins: []string{"root"},
|
||||
NodeLabels: types.Labels{
|
||||
"owner": []string{"admins"},
|
||||
},
|
||||
ReviewRequests: &types.AccessReviewConditions{
|
||||
Roles: []string{"admins"},
|
||||
},
|
||||
},
|
||||
},
|
||||
// operators can request the admins role
|
||||
"operators": types.RoleSpecV5{
|
||||
Allow: types.RoleConditions{
|
||||
Request: &types.AccessRequestConditions{
|
||||
Roles: []string{"admins"},
|
||||
},
|
||||
},
|
||||
},
|
||||
// responders can request the admins role but only with a
|
||||
// search-based request limited to specific resources
|
||||
"responders": types.RoleSpecV5{
|
||||
Allow: types.RoleConditions{
|
||||
Request: &types.AccessRequestConditions{
|
||||
SearchAsRoles: []string{"admins"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"empty": types.RoleSpecV5{},
|
||||
}
|
||||
for roleName, roleSpec := range roleSpecs {
|
||||
role, err := types.NewRole(roleName, roleSpec)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = server.Auth().UpsertRole(ctx, role)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
userDesc := map[string]struct {
|
||||
roles []string
|
||||
}{
|
||||
"admin": {
|
||||
roles: []string{"admins"},
|
||||
},
|
||||
"responder": {
|
||||
roles: []string{"responders"},
|
||||
},
|
||||
"operator": {
|
||||
roles: []string{"operators"},
|
||||
},
|
||||
"nobody": {
|
||||
roles: []string{"empty"},
|
||||
},
|
||||
}
|
||||
for name, desc := range userDesc {
|
||||
user, err := types.NewUser(name)
|
||||
require.NoError(t, err)
|
||||
user.SetRoles(desc.roles)
|
||||
err = server.Auth().UpsertUser(user)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
resourceIDs := []types.ResourceID{{
|
||||
ClusterName: "cluster-one",
|
||||
Kind: "node",
|
||||
Name: "some-uuid",
|
||||
}}
|
||||
|
||||
privKey, pubKey, err := native.GenerateKeyPair()
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
requester string
|
||||
reviewer string
|
||||
expectRequestableRoles []string
|
||||
requestRoles []string
|
||||
expectRoles []string
|
||||
requestResources []types.ResourceID
|
||||
expectRequestError error
|
||||
expectReviewError error
|
||||
}{
|
||||
{
|
||||
desc: "role request",
|
||||
requester: "operator",
|
||||
reviewer: "admin",
|
||||
expectRequestableRoles: []string{"admins"},
|
||||
requestRoles: []string{"admins"},
|
||||
expectRoles: []string{"operators", "admins"},
|
||||
},
|
||||
{
|
||||
desc: "no requestable roles",
|
||||
requester: "nobody",
|
||||
requestRoles: []string{"admins"},
|
||||
expectRequestError: trace.BadParameter(`user "nobody" can not request role "admins"`),
|
||||
},
|
||||
{
|
||||
desc: "role not allowed",
|
||||
requester: "operator",
|
||||
expectRequestableRoles: []string{"admins"},
|
||||
requestRoles: []string{"super-admins"},
|
||||
expectRequestError: trace.BadParameter(`user "operator" can not request role "super-admins"`),
|
||||
},
|
||||
{
|
||||
desc: "review own request",
|
||||
requester: "operator",
|
||||
reviewer: "operator",
|
||||
expectRequestableRoles: []string{"admins"},
|
||||
requestRoles: []string{"admins"},
|
||||
expectReviewError: trace.AccessDenied(`user "operator" cannot submit reviews`),
|
||||
},
|
||||
{
|
||||
desc: "search-based request",
|
||||
requester: "responder",
|
||||
reviewer: "admin",
|
||||
requestResources: resourceIDs,
|
||||
expectRoles: []string{"responders", "admins"},
|
||||
},
|
||||
{
|
||||
desc: "no search_as_roles",
|
||||
requester: "nobody",
|
||||
requestResources: resourceIDs,
|
||||
expectRequestError: trace.AccessDenied(`user does not have any "search_as_roles" which are valid for this request`),
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
requester := TestUser(tc.requester)
|
||||
requesterClient, err := server.NewClient(requester)
|
||||
require.NoError(t, err)
|
||||
|
||||
// generateCerts executes a GenerateUserCerts request, optionally applying
|
||||
// one or more access-requests to the certificate.
|
||||
generateCerts := func(reqIDs ...string) (*proto.Certs, error) {
|
||||
return requesterClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
||||
PublicKey: pubKey,
|
||||
Username: tc.requester,
|
||||
Expires: time.Now().Add(time.Hour).UTC(),
|
||||
Format: constants.CertificateFormatStandard,
|
||||
AccessRequests: reqIDs,
|
||||
})
|
||||
}
|
||||
|
||||
// sanity check we can get certs with no access request
|
||||
certs, err := generateCerts()
|
||||
require.NoError(t, err)
|
||||
|
||||
// should have no logins, requests, or resources in ssh cert
|
||||
checkCerts(t, certs, userDesc[tc.requester].roles, nil, nil, nil)
|
||||
|
||||
// requestable roles should be correct
|
||||
caps, err := requesterClient.GetAccessCapabilities(ctx, types.AccessCapabilitiesRequest{
|
||||
RequestableRoles: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectRequestableRoles, caps.RequestableRoles)
|
||||
|
||||
// create the access request object
|
||||
req, err := services.NewAccessRequestWithResources(tc.requester, tc.requestRoles, tc.requestResources)
|
||||
require.NoError(t, err)
|
||||
|
||||
// send the request to the auth server
|
||||
err = requesterClient.CreateAccessRequest(ctx, req)
|
||||
require.ErrorIs(t, err, tc.expectRequestError)
|
||||
if tc.expectRequestError != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// try logging in with request in PENDING state (should fail)
|
||||
_, err = generateCerts(req.GetName())
|
||||
require.ErrorIs(t, err, trace.AccessDenied("access request %q is awaiting approval", req.GetName()))
|
||||
|
||||
reviewer := TestUser(tc.reviewer)
|
||||
reviewerClient, err := server.NewClient(reviewer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// approve the request
|
||||
req, err = reviewerClient.SubmitAccessReview(ctx, types.AccessReviewSubmission{
|
||||
RequestID: req.GetName(),
|
||||
Review: types.AccessReview{
|
||||
ProposedState: types.RequestState_APPROVED,
|
||||
},
|
||||
})
|
||||
require.ErrorIs(t, err, tc.expectReviewError)
|
||||
if tc.expectReviewError != nil {
|
||||
return
|
||||
}
|
||||
require.Equal(t, types.RequestState_APPROVED, req.GetState())
|
||||
|
||||
// log in now that request has been approved
|
||||
certs, err = generateCerts(req.GetName())
|
||||
require.NoError(t, err)
|
||||
|
||||
// cert should have login from requested role, the access request
|
||||
// should be in the cert, and any requested resources should be
|
||||
// encoded in the cert
|
||||
checkCerts(t,
|
||||
certs,
|
||||
tc.expectRoles,
|
||||
[]string{"root"},
|
||||
[]string{req.GetName()},
|
||||
tc.requestResources)
|
||||
|
||||
elevatedCert, err := tls.X509KeyPair(certs.TLS, privKey)
|
||||
require.NoError(t, err)
|
||||
elevatedClient := server.NewClientWithCert(elevatedCert)
|
||||
|
||||
// renew elevated certs
|
||||
newCerts, err := elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
||||
PublicKey: pubKey,
|
||||
Username: tc.requester,
|
||||
Expires: time.Now().Add(time.Hour).UTC(),
|
||||
// no new access requests
|
||||
AccessRequests: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// inspite of providing no access requests, we still have elevated
|
||||
// roles and the certicate shows the original access request
|
||||
checkCerts(t,
|
||||
newCerts,
|
||||
tc.expectRoles,
|
||||
[]string{"root"},
|
||||
[]string{req.GetName()},
|
||||
tc.requestResources)
|
||||
|
||||
// attempt to apply request in DENIED state (should fail)
|
||||
require.NoError(t, server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
|
||||
RequestID: req.GetName(),
|
||||
State: types.RequestState_DENIED,
|
||||
}))
|
||||
_, err = generateCerts(req.GetName())
|
||||
require.ErrorIs(t, err, trace.AccessDenied("access request %q has been denied", req.GetName()))
|
||||
|
||||
// ensure that once in the DENIED state, a request cannot be set back to PENDING state.
|
||||
require.Error(t, server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
|
||||
RequestID: req.GetName(),
|
||||
State: types.RequestState_PENDING,
|
||||
}))
|
||||
|
||||
// ensure that once in the DENIED state, a request cannot be set back to APPROVED state.
|
||||
require.Error(t, server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
|
||||
RequestID: req.GetName(),
|
||||
State: types.RequestState_APPROVED,
|
||||
}))
|
||||
|
||||
// ensure that identities with requests in the DENIED state can't reissue new certs.
|
||||
_, err = elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
||||
PublicKey: pubKey,
|
||||
Username: tc.requester,
|
||||
Expires: time.Now().Add(time.Hour).UTC(),
|
||||
// no new access requests
|
||||
AccessRequests: nil,
|
||||
})
|
||||
require.ErrorIs(t, err, trace.AccessDenied("access request %q has been denied", req.GetName()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// checkCerts checks that the ssh and tls certs include the given roles, logins,
|
||||
// accessRequests, and resourceIDs
|
||||
func checkCerts(t *testing.T,
|
||||
certs *proto.Certs,
|
||||
roles []string,
|
||||
logins []string,
|
||||
accessRequests []string,
|
||||
resourceIDs []types.ResourceID,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
// Parse SSH cert.
|
||||
sshCert, err := sshutils.ParseCertificate(certs.SSH)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse TLS cert.
|
||||
tlsCert, err := tlsca.ParseCertificatePEM(certs.TLS)
|
||||
require.NoError(t, err)
|
||||
tlsIdentity, err := tlsca.FromSubject(tlsCert.Subject, tlsCert.NotAfter)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make sure both certs have the expected roles.
|
||||
rawSSHCertRoles := sshCert.Permissions.Extensions[teleport.CertExtensionTeleportRoles]
|
||||
sshCertRoles, err := services.UnmarshalCertRoles(rawSSHCertRoles)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, roles, sshCertRoles)
|
||||
require.ElementsMatch(t, roles, tlsIdentity.Groups)
|
||||
|
||||
// Make sure both certs have the expected logins/principals.
|
||||
for _, certLogins := range [][]string{sshCert.ValidPrincipals, tlsIdentity.Principals} {
|
||||
// filter out invalid logins placed in the cert
|
||||
validCertLogins := []string{}
|
||||
for _, certLogin := range certLogins {
|
||||
if !strings.HasPrefix(certLogin, "-teleport") {
|
||||
validCertLogins = append(validCertLogins, certLogin)
|
||||
}
|
||||
}
|
||||
require.ElementsMatch(t, logins, validCertLogins)
|
||||
}
|
||||
|
||||
// Make sure both certs have the expected access requests, if any.
|
||||
rawSSHCertAccessRequests := sshCert.Permissions.Extensions[teleport.CertExtensionTeleportActiveRequests]
|
||||
sshCertAccessRequests := services.RequestIDs{}
|
||||
if len(rawSSHCertAccessRequests) > 0 {
|
||||
require.NoError(t, sshCertAccessRequests.Unmarshal([]byte(rawSSHCertAccessRequests)))
|
||||
}
|
||||
require.ElementsMatch(t, accessRequests, sshCertAccessRequests.AccessRequests)
|
||||
require.ElementsMatch(t, accessRequests, tlsIdentity.ActiveRequests)
|
||||
|
||||
// Make sure both certs have the expected allowed resources, if any.
|
||||
for _, certResourcesStr := range []string{
|
||||
sshCert.Permissions.Extensions[teleport.CertExtensionAllowedResources],
|
||||
tlsIdentity.AllowedResourceIDs,
|
||||
} {
|
||||
certResources, err := services.ResourceIDsFromString(certResourcesStr)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, resourceIDs, certResources)
|
||||
}
|
||||
}
|
|
@ -745,6 +745,9 @@ type certRequest struct {
|
|||
// activeRequests tracks privilege escalation requests applied
|
||||
// during the construction of the certificate.
|
||||
activeRequests services.RequestIDs
|
||||
// requestedResourceIDs lists the resources to which access has been
|
||||
// requested.
|
||||
requestedResourceIDs []types.ResourceID
|
||||
// appSessionID is the session ID of the application session.
|
||||
appSessionID string
|
||||
// appPublicAddr is the public address of the application.
|
||||
|
@ -1053,6 +1056,11 @@ func (a *Server) generateUserCert(req certRequest) (*proto.Certs, error) {
|
|||
// All users have access to this and join RBAC rules are checked after the connection is established.
|
||||
allowedLogins = append(allowedLogins, "-teleport-internal-join")
|
||||
|
||||
requestedResourcesStr, err := services.ResourceIDsToString(req.requestedResourceIDs)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
params := services.UserCertParams{
|
||||
CASigner: caSigner,
|
||||
CASigningAlg: sshutils.GetSigningAlgName(userCA),
|
||||
|
@ -1075,6 +1083,7 @@ func (a *Server) generateUserCert(req certRequest) (*proto.Certs, error) {
|
|||
Renewable: req.renewable,
|
||||
Generation: req.generation,
|
||||
CertificateExtensions: req.checker.CertificateExtensions(),
|
||||
AllowedResourceIDs: requestedResourcesStr,
|
||||
}
|
||||
sshCert, err := a.Authority.GenerateUserCert(params)
|
||||
if err != nil {
|
||||
|
@ -1146,15 +1155,16 @@ func (a *Server) generateUserCert(req certRequest) (*proto.Certs, error) {
|
|||
Username: req.dbUser,
|
||||
Database: req.dbName,
|
||||
},
|
||||
DatabaseNames: dbNames,
|
||||
DatabaseUsers: dbUsers,
|
||||
MFAVerified: req.mfaVerified,
|
||||
ClientIP: req.clientIP,
|
||||
AWSRoleARNs: roleARNs,
|
||||
ActiveRequests: req.activeRequests.AccessRequests,
|
||||
DisallowReissue: req.disallowReissue,
|
||||
Renewable: req.renewable,
|
||||
Generation: req.generation,
|
||||
DatabaseNames: dbNames,
|
||||
DatabaseUsers: dbUsers,
|
||||
MFAVerified: req.mfaVerified,
|
||||
ClientIP: req.clientIP,
|
||||
AWSRoleARNs: roleARNs,
|
||||
ActiveRequests: req.activeRequests.AccessRequests,
|
||||
DisallowReissue: req.disallowReissue,
|
||||
Renewable: req.renewable,
|
||||
Generation: req.generation,
|
||||
AllowedResourceIDs: requestedResourcesStr,
|
||||
}
|
||||
subject, err := identity.Subject()
|
||||
if err != nil {
|
||||
|
@ -1819,19 +1829,21 @@ func (a *Server) ExtendWebSession(ctx context.Context, req WebSessionReq, identi
|
|||
}
|
||||
|
||||
accessRequests := identity.ActiveRequests
|
||||
requestedResourceIDs := []types.ResourceID{}
|
||||
if req.AccessRequestID != "" {
|
||||
newRoles, requestExpiry, err := a.getRolesAndExpiryFromAccessRequest(ctx, req.User, req.AccessRequestID)
|
||||
accessRequest, err := a.getValidatedAccessRequest(ctx, req.User, req.AccessRequestID)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
roles = append(roles, newRoles...)
|
||||
roles = append(roles, accessRequest.GetRoles()...)
|
||||
roles = apiutils.Deduplicate(roles)
|
||||
accessRequests = apiutils.Deduplicate(append(accessRequests, req.AccessRequestID))
|
||||
requestedResourceIDs = accessRequest.GetRequestedResourceIDs()
|
||||
|
||||
// Let session expire with the shortest expiry time.
|
||||
if expiresAt.After(requestExpiry) {
|
||||
expiresAt = requestExpiry
|
||||
if expiresAt.After(accessRequest.GetAccessExpiry()) {
|
||||
expiresAt = accessRequest.GetAccessExpiry()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1862,11 +1874,12 @@ func (a *Server) ExtendWebSession(ctx context.Context, req WebSessionReq, identi
|
|||
|
||||
sessionTTL := utils.ToTTL(a.clock, expiresAt)
|
||||
sess, err := a.NewWebSession(types.NewWebSessionRequest{
|
||||
User: req.User,
|
||||
Roles: roles,
|
||||
Traits: traits,
|
||||
SessionTTL: sessionTTL,
|
||||
AccessRequests: accessRequests,
|
||||
User: req.User,
|
||||
Roles: roles,
|
||||
Traits: traits,
|
||||
SessionTTL: sessionTTL,
|
||||
AccessRequests: accessRequests,
|
||||
RequestedResourceIDs: requestedResourceIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -1882,7 +1895,7 @@ func (a *Server) ExtendWebSession(ctx context.Context, req WebSessionReq, identi
|
|||
return sess, nil
|
||||
}
|
||||
|
||||
func (a *Server) getRolesAndExpiryFromAccessRequest(ctx context.Context, user, accessRequestID string) ([]string, time.Time, error) {
|
||||
func (a *Server) getValidatedAccessRequest(ctx context.Context, user, accessRequestID string) (types.AccessRequest, error) {
|
||||
reqFilter := types.AccessRequestFilter{
|
||||
User: user,
|
||||
ID: accessRequestID,
|
||||
|
@ -1890,37 +1903,32 @@ func (a *Server) getRolesAndExpiryFromAccessRequest(ctx context.Context, user, a
|
|||
|
||||
reqs, err := a.GetAccessRequests(ctx, reqFilter)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, trace.Wrap(err)
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if len(reqs) < 1 {
|
||||
return nil, time.Time{}, trace.NotFound("access request %q not found", accessRequestID)
|
||||
return nil, trace.NotFound("access request %q not found", accessRequestID)
|
||||
}
|
||||
|
||||
req := reqs[0]
|
||||
|
||||
if len(req.GetRequestedResourceIDs()) > 0 {
|
||||
// TODO(nic): handle search-based access requests #10887
|
||||
return nil, time.Time{}, trace.BadParameter("search-based access requests are not yet supported")
|
||||
}
|
||||
|
||||
if !req.GetState().IsApproved() {
|
||||
if req.GetState().IsDenied() {
|
||||
return nil, time.Time{}, trace.AccessDenied("access request %q has been denied", accessRequestID)
|
||||
return nil, trace.AccessDenied("access request %q has been denied", accessRequestID)
|
||||
}
|
||||
return nil, time.Time{}, trace.BadParameter("access request %q is awaiting approval", accessRequestID)
|
||||
return nil, trace.AccessDenied("access request %q is awaiting approval", accessRequestID)
|
||||
}
|
||||
|
||||
if err := services.ValidateAccessRequestForUser(a, req); err != nil {
|
||||
return nil, time.Time{}, trace.Wrap(err)
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
accessExpiry := req.GetAccessExpiry()
|
||||
if accessExpiry.Before(a.GetClock().Now()) {
|
||||
return nil, time.Time{}, trace.BadParameter("access request %q has expired", accessRequestID)
|
||||
return nil, trace.BadParameter("access request %q has expired", accessRequestID)
|
||||
}
|
||||
|
||||
return req.GetRoles(), accessExpiry, nil
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// CreateWebSession creates a new web session for user without any
|
||||
|
@ -2329,12 +2337,13 @@ func (a *Server) NewWebSession(req types.NewWebSessionRequest) (types.WebSession
|
|||
sessionTTL = checker.AdjustSessionTTL(apidefaults.CertDuration)
|
||||
}
|
||||
certs, err := a.generateUserCert(certRequest{
|
||||
user: user,
|
||||
ttl: sessionTTL,
|
||||
publicKey: pub,
|
||||
checker: checker,
|
||||
traits: req.Traits,
|
||||
activeRequests: services.RequestIDs{AccessRequests: req.AccessRequests},
|
||||
user: user,
|
||||
ttl: sessionTTL,
|
||||
publicKey: pub,
|
||||
checker: checker,
|
||||
traits: req.Traits,
|
||||
activeRequests: services.RequestIDs{AccessRequests: req.AccessRequests},
|
||||
requestedResourceIDs: req.RequestedResourceIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
|
|
@ -2143,19 +2143,25 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
|
|||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
var requestedResourceIDs []types.ResourceID
|
||||
if len(req.AccessRequests) > 0 {
|
||||
// add any applicable access request values.
|
||||
req.AccessRequests = apiutils.Deduplicate(req.AccessRequests)
|
||||
for _, reqID := range req.AccessRequests {
|
||||
newRoles, accessRequestExpiry, err := a.authServer.getRolesAndExpiryFromAccessRequest(ctx, req.Username, reqID)
|
||||
accessRequest, err := a.authServer.getValidatedAccessRequest(ctx, req.Username, reqID)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if accessRequestExpiry.Before(req.Expires) {
|
||||
if accessRequest.GetAccessExpiry().Before(req.Expires) {
|
||||
// cannot generate a cert that would outlive the access request
|
||||
req.Expires = accessRequestExpiry
|
||||
req.Expires = accessRequest.GetAccessExpiry()
|
||||
}
|
||||
roles = append(roles, newRoles...)
|
||||
roles = append(roles, accessRequest.GetRoles()...)
|
||||
|
||||
if len(accessRequest.GetRequestedResourceIDs()) > 0 && len(requestedResourceIDs) > 0 {
|
||||
return nil, trace.BadParameter("cannot generate certificate with multiple search-based access requests")
|
||||
}
|
||||
requestedResourceIDs = accessRequest.GetRequestedResourceIDs()
|
||||
}
|
||||
// nothing prevents an access-request from including roles already possessed by the
|
||||
// user, so we must make sure to trim duplicate roles.
|
||||
|
@ -2257,6 +2263,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
|
|||
activeRequests: services.RequestIDs{
|
||||
AccessRequests: req.AccessRequests,
|
||||
},
|
||||
requestedResourceIDs: requestedResourceIDs,
|
||||
}
|
||||
if user.GetName() != a.context.User.GetName() {
|
||||
certReq.impersonator = a.context.User.GetName()
|
||||
|
|
|
@ -274,6 +274,9 @@ func (k *Keygen) GenerateUserCertWithoutValidation(c services.UserCertParams) ([
|
|||
if c.Generation > 0 {
|
||||
cert.Permissions.Extensions[teleport.CertExtensionGeneration] = fmt.Sprint(c.Generation)
|
||||
}
|
||||
if c.AllowedResourceIDs != "" {
|
||||
cert.Permissions.Extensions[teleport.CertExtensionAllowedResources] = c.AllowedResourceIDs
|
||||
}
|
||||
|
||||
for _, extension := range c.CertificateExtensions {
|
||||
// TODO(lxea): update behavior when non ssh, non extensions are supported.
|
||||
|
|
|
@ -1350,195 +1350,6 @@ func (s *TLSSuite) TestGetCertAuthority(c *check.C) {
|
|||
fixtures.ExpectAccessDenied(c, err)
|
||||
}
|
||||
|
||||
func (s *TLSSuite) TestAccessRequest(c *check.C) {
|
||||
priv, pub, err := native.GenerateKeyPair()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
// make sure we can parse the private and public key
|
||||
privateKey, err := ssh.ParseRawPrivateKey(priv)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = tlsca.MarshalPublicKeyFromPrivateKeyPEM(privateKey)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, _, _, _, err = ssh.ParseAuthorizedKey(pub)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
// create a user with one requestable role
|
||||
user := "user1"
|
||||
role := "some-role"
|
||||
_, err = CreateUserRoleAndRequestable(s.server.Auth(), user, role)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
testUser := TestUser(user)
|
||||
testUser.TTL = time.Hour
|
||||
userClient, err := s.server.NewClient(testUser)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
// Verify that user has correct requestable roles
|
||||
caps, err := userClient.GetAccessCapabilities(context.TODO(), types.AccessCapabilitiesRequest{
|
||||
RequestableRoles: true,
|
||||
})
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(caps.RequestableRoles, check.DeepEquals, []string{role})
|
||||
|
||||
// create a user with no requestable roles
|
||||
user2, _, err := CreateUserAndRole(s.server.Auth(), "user2", []string{"user2"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
testUser2 := TestUser(user2.GetName())
|
||||
testUser2.TTL = time.Hour
|
||||
userClient2, err := s.server.NewClient(testUser2)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
// verify that no requestable roles are shown for user2
|
||||
caps2, err := userClient2.GetAccessCapabilities(context.TODO(), types.AccessCapabilitiesRequest{
|
||||
RequestableRoles: true,
|
||||
})
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(caps2.RequestableRoles, check.HasLen, 0)
|
||||
|
||||
// create an allowable access request for user1
|
||||
req, err := services.NewAccessRequest(user, role)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
c.Assert(userClient.CreateAccessRequest(context.TODO(), req), check.IsNil)
|
||||
|
||||
// sanity check; ensure that roles for which no `allow` directive
|
||||
// exists cannot be requested.
|
||||
badReq, err := services.NewAccessRequest(user, "some-fake-role")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(userClient.CreateAccessRequest(context.TODO(), badReq), check.NotNil)
|
||||
|
||||
// generateCerts executes a GenerateUserCerts request, optionally applying
|
||||
// one or more access-requests to the certificate.
|
||||
generateCerts := func(reqIDs ...string) (*proto.Certs, error) {
|
||||
return userClient.GenerateUserCerts(context.TODO(), proto.UserCertsRequest{
|
||||
PublicKey: pub,
|
||||
Username: user,
|
||||
Expires: time.Now().Add(time.Hour).UTC(),
|
||||
Format: constants.CertificateFormatStandard,
|
||||
AccessRequests: reqIDs,
|
||||
})
|
||||
}
|
||||
|
||||
// certContainsRole checks if a PEM encoded TLS cert contains the
|
||||
// specified role.
|
||||
certContainsRole := func(tlsCert []byte, role string) bool {
|
||||
cert, err := tlsca.ParseCertificatePEM(tlsCert)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
return apiutils.SliceContainsStr(identity.Groups, role)
|
||||
}
|
||||
|
||||
// certRequests extracts the active requests from a PEM encoded TLS cert.
|
||||
certRequests := func(tlsCert []byte) []string {
|
||||
cert, err := tlsca.ParseCertificatePEM(tlsCert)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
return identity.ActiveRequests
|
||||
}
|
||||
|
||||
// certLogins extracts the logins from an ssh certificate
|
||||
certLogins := func(sshCert []byte) []string {
|
||||
cert, err := sshutils.ParseCertificate(sshCert)
|
||||
c.Assert(err, check.IsNil)
|
||||
return cert.ValidPrincipals
|
||||
}
|
||||
|
||||
// sanity check; ensure that role is not held if no request is applied.
|
||||
userCerts, err := generateCerts()
|
||||
c.Assert(err, check.IsNil)
|
||||
if certContainsRole(userCerts.TLS, role) {
|
||||
c.Errorf("unexpected role %s", role)
|
||||
}
|
||||
// ensure that the default identity doesn't have any active requests
|
||||
c.Assert(certRequests(userCerts.TLS), check.HasLen, 0)
|
||||
|
||||
// verify that cert for user with no static logins is generated with
|
||||
// exactly two logins and that the one that isn't a join principal is an invalid unix login (indicated
|
||||
// by preceding dash (-).
|
||||
logins := certLogins(userCerts.SSH)
|
||||
c.Assert(len(logins), check.Equals, 2)
|
||||
c.Assert(rune(logins[0][0]), check.Equals, '-')
|
||||
|
||||
// attempt to apply request in PENDING state (should fail)
|
||||
_, err = generateCerts(req.GetName())
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// verify that user does not have the ability to approve their own request (not a special case, this
|
||||
// user just wasn't created with the necessary roles for request management).
|
||||
c.Assert(userClient.SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_APPROVED}), check.NotNil)
|
||||
|
||||
// attempt to apply request in APPROVED state (should succeed)
|
||||
c.Assert(s.server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_APPROVED}), check.IsNil)
|
||||
userCerts, err = generateCerts(req.GetName())
|
||||
c.Assert(err, check.IsNil)
|
||||
// ensure that the requested role was actually applied to the cert
|
||||
if !certContainsRole(userCerts.TLS, role) {
|
||||
c.Errorf("missing requested role %s", role)
|
||||
}
|
||||
// ensure that the request is stored in the certs
|
||||
c.Assert(certRequests(userCerts.TLS), check.DeepEquals, []string{req.GetName()})
|
||||
|
||||
// verify that dynamically applied role granted a login,
|
||||
// which is is valid and has replaced the dummy login.
|
||||
logins = certLogins(userCerts.SSH)
|
||||
c.Assert(len(logins), check.Equals, 2)
|
||||
c.Assert(rune(logins[0][0]), check.Not(check.Equals), '-')
|
||||
|
||||
elevatedCert, err := tls.X509KeyPair(userCerts.TLS, priv)
|
||||
c.Assert(err, check.IsNil)
|
||||
elevatedClient := s.server.NewClientWithCert(elevatedCert)
|
||||
|
||||
newCerts, err := elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
||||
PublicKey: pub,
|
||||
Username: user,
|
||||
Expires: time.Now().Add(time.Hour).UTC(),
|
||||
Format: constants.CertificateFormatStandard,
|
||||
// no new access requests
|
||||
AccessRequests: nil,
|
||||
})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
// in spite of having no access requests, we still have elevated roles...
|
||||
if !certContainsRole(newCerts.TLS, role) {
|
||||
c.Errorf("missing requested role %s", role)
|
||||
}
|
||||
// ...and the certificate shows the access request
|
||||
c.Assert(certRequests(newCerts.TLS), check.DeepEquals, []string{req.GetName()})
|
||||
|
||||
// attempt to apply request in DENIED state (should fail)
|
||||
c.Assert(s.server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_DENIED}), check.IsNil)
|
||||
_, err = generateCerts(req.GetName())
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
// ensure that once in the DENIED state, a request cannot be set back to PENDING state.
|
||||
c.Assert(s.server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_PENDING}), check.NotNil)
|
||||
|
||||
// ensure that once in the DENIED state, a request cannot be set back to APPROVED state.
|
||||
c.Assert(s.server.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{RequestID: req.GetName(), State: types.RequestState_APPROVED}), check.NotNil)
|
||||
|
||||
// ensure that identities with requests in the DENIED state can't reissue new certs.
|
||||
_, err = elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
||||
PublicKey: pub,
|
||||
Username: user,
|
||||
Expires: time.Now().Add(time.Hour).UTC(),
|
||||
Format: constants.CertificateFormatStandard,
|
||||
// no new access requests
|
||||
AccessRequests: nil,
|
||||
})
|
||||
c.Assert(err, check.NotNil)
|
||||
}
|
||||
|
||||
func (s *TLSSuite) TestPluginData(c *check.C) {
|
||||
priv, pub, err := native.GenerateKeyPair()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
|
|
@ -1285,6 +1285,9 @@ func MarshalAccessRequest(accessRequest types.AccessRequest, opts ...MarshalOpti
|
|||
}
|
||||
|
||||
func ResourceIDsToString(ids []types.ResourceID) (string, error) {
|
||||
if len(ids) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
bytes, err := utils.FastMarshal(ids)
|
||||
if err != nil {
|
||||
return "", trace.BadParameter("failed to marshal resource IDs to JSON: %v", err)
|
||||
|
@ -1293,6 +1296,9 @@ func ResourceIDsToString(ids []types.ResourceID) (string, error) {
|
|||
}
|
||||
|
||||
func ResourceIDsFromString(raw string) ([]types.ResourceID, error) {
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
resourceIDs := []types.ResourceID{}
|
||||
if err := utils.FastUnmarshal([]byte(raw), &resourceIDs); err != nil {
|
||||
return nil, trace.BadParameter("failed to parse resource IDs from JSON: %v", err)
|
||||
|
|
|
@ -298,6 +298,8 @@ type UserCertParams struct {
|
|||
Renewable bool
|
||||
// Generation counts the number of times a certificate has been renewed.
|
||||
Generation uint64
|
||||
// AllowedResourceIDs lists the resources the user should be able to access.
|
||||
AllowedResourceIDs string
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults checks the user certificate parameters
|
||||
|
|
|
@ -160,6 +160,13 @@ func (h *AuthHandlers) CreateIdentityContext(sconn *ssh.ServerConn) (IdentityCon
|
|||
}
|
||||
identity.Generation = generation
|
||||
}
|
||||
if allowedResourcesStr, ok := certificate.Extensions[teleport.CertExtensionAllowedResources]; ok {
|
||||
allowedResourceIDs, err := services.ResourceIDsFromString(allowedResourcesStr)
|
||||
if err != nil {
|
||||
return IdentityContext{}, trace.Wrap(err)
|
||||
}
|
||||
identity.AllowedResourceIDs = allowedResourceIDs
|
||||
}
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -214,6 +214,10 @@ type IdentityContext struct {
|
|||
// Generation counts the number of times this identity's certificate has
|
||||
// been renewed.
|
||||
Generation uint64
|
||||
|
||||
// AllowedResourceIDs lists the resources this identity should be allowed to
|
||||
// access
|
||||
AllowedResourceIDs []types.ResourceID
|
||||
}
|
||||
|
||||
// ServerContext holds session specific context, such as SSH auth agents, PTYs,
|
||||
|
|
|
@ -141,6 +141,9 @@ type Identity struct {
|
|||
Renewable bool
|
||||
// Generation counts the number of times this certificate has been renewed.
|
||||
Generation uint64
|
||||
// AllowedResourceIDs lists the resources the identity should be allowed to
|
||||
// access.
|
||||
AllowedResourceIDs string
|
||||
}
|
||||
|
||||
// RouteToApp holds routing information for applications.
|
||||
|
@ -361,6 +364,10 @@ var (
|
|||
// requests to generate new certificates using this certificate should be
|
||||
// denied.
|
||||
DisallowReissueASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 2, 9}
|
||||
|
||||
// AllowedResourcesASN1ExtensionOID is an extension OID used to list the
|
||||
// resources which the certificate should be able to grant access to
|
||||
AllowedResourcesASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 2, 10}
|
||||
)
|
||||
|
||||
// Subject converts identity to X.509 subject name
|
||||
|
@ -565,6 +572,15 @@ func (id *Identity) Subject() (pkix.Name, error) {
|
|||
)
|
||||
}
|
||||
|
||||
if id.AllowedResourceIDs != "" {
|
||||
subject.ExtraNames = append(subject.ExtraNames,
|
||||
pkix.AttributeTypeAndValue{
|
||||
Type: AllowedResourcesASN1ExtensionOID,
|
||||
Value: id.AllowedResourceIDs,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return subject, nil
|
||||
}
|
||||
|
||||
|
@ -710,6 +726,11 @@ func FromSubject(subject pkix.Name, expires time.Time) (*Identity, error) {
|
|||
}
|
||||
id.Generation = generation
|
||||
}
|
||||
case attr.Type.Equal(AllowedResourcesASN1ExtensionOID):
|
||||
val, ok := attr.Value.(string)
|
||||
if ok {
|
||||
id.AllowedResourceIDs = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue