package acl import ( "context" "crypto/ecdsa" "errors" "fmt" "io" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/engine" eaclV2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/eacl/v2" v2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/v2" bearerSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" eaclSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" ) // Checker implements v2.ACLChecker interfaces and provides // ACL/eACL validation functionality. type Checker struct { eaclSrc container.EACLSource validator *eaclSDK.Validator localStorage *engine.StorageEngine state netmap.State } type localStorage struct { ls *engine.StorageEngine } func (s *localStorage) Head(ctx context.Context, addr oid.Address) (*objectSDK.Object, error) { if s.ls == nil { return nil, io.ErrUnexpectedEOF } return engine.Head(ctx, s.ls, addr) } // Various EACL check errors. var ( errEACLDeniedByRule = errors.New("denied by rule") errBearerExpired = errors.New("bearer token has expired") errBearerInvalidSignature = errors.New("bearer token has invalid signature") errBearerInvalidContainerID = errors.New("bearer token was created for another container") errBearerNotSignedByOwner = errors.New("bearer token is not signed by the container owner") errBearerInvalidOwner = errors.New("bearer token owner differs from the request sender") ) // NewChecker creates Checker. // Panics if at least one of the parameter is nil. func NewChecker( state netmap.State, eaclSrc container.EACLSource, validator *eaclSDK.Validator, localStorage *engine.StorageEngine, ) *Checker { return &Checker{ eaclSrc: eaclSrc, validator: validator, localStorage: localStorage, state: state, } } // CheckEACL is a main check function for extended ACL. func (c *Checker) CheckEACL(msg any, reqInfo v2.RequestInfo) error { basicACL := reqInfo.BasicACL() if !basicACL.Extendable() { return nil } bearerTok := reqInfo.Bearer() impersonate := bearerTok != nil && bearerTok.Impersonate() // if bearer token is not allowed, then ignore it if impersonate || !basicACL.AllowedBearerRules(reqInfo.Operation()) { reqInfo.CleanBearer() } var table eaclSDK.Table cnr := reqInfo.ContainerID() if bearerTok == nil { eaclInfo, err := c.eaclSrc.GetEACL(cnr) if err != nil { if client.IsErrEACLNotFound(err) { return nil } return err } table = *eaclInfo.Value } else { table = bearerTok.EACLTable() } // if bearer token is not present, isValidBearer returns true if err := isValidBearer(reqInfo, c.state); err != nil { return err } hdrSrc, err := c.getHeaderSource(cnr, msg, reqInfo) if err != nil { return err } eaclRole := getRole(reqInfo) action, _ := c.validator.CalculateAction(new(eaclSDK.ValidationUnit). WithRole(eaclRole). WithOperation(eaclSDK.Operation(reqInfo.Operation())). WithContainerID(&cnr). WithSenderKey(reqInfo.SenderKey()). WithHeaderSource(hdrSrc). WithEACLTable(&table), ) if action != eaclSDK.ActionAllow { return errEACLDeniedByRule } return nil } func getRole(reqInfo v2.RequestInfo) eaclSDK.Role { var eaclRole eaclSDK.Role switch op := reqInfo.RequestRole(); op { default: eaclRole = eaclSDK.Role(op) case acl.RoleOwner: eaclRole = eaclSDK.RoleUser case acl.RoleInnerRing, acl.RoleContainer: eaclRole = eaclSDK.RoleSystem case acl.RoleOthers: eaclRole = eaclSDK.RoleOthers } return eaclRole } func (c *Checker) getHeaderSource(cnr cid.ID, msg any, reqInfo v2.RequestInfo) (eaclSDK.TypedHeaderSource, error) { var xHeaderSource eaclV2.XHeaderSource if req, ok := msg.(eaclV2.Request); ok { xHeaderSource = eaclV2.NewRequestXHeaderSource(req) } else { xHeaderSource = eaclV2.NewResponseXHeaderSource(msg.(eaclV2.Response), reqInfo.Request().(eaclV2.Request)) } hdrSrc, err := eaclV2.NewMessageHeaderSource(&localStorage{ls: c.localStorage}, xHeaderSource, cnr, eaclV2.WithOID(reqInfo.ObjectID())) if err != nil { return nil, fmt.Errorf("can't parse headers: %w", err) } return hdrSrc, nil } // isValidBearer checks whether bearer token was correctly signed by authorized // entity. This method might be defined on whole ACL service because it will // require fetching current epoch to check lifetime. func isValidBearer(reqInfo v2.RequestInfo, st netmap.State) error { ownerCnr := reqInfo.ContainerOwner() token := reqInfo.Bearer() // 0. Check if bearer token is present in reqInfo. if token == nil { return nil } // 1. First check token lifetime. Simplest verification. if token.InvalidAt(st.CurrentEpoch()) { return errBearerExpired } // 2. Then check if bearer token is signed correctly. if !token.VerifySignature() { return errBearerInvalidSignature } // 3. Then check if container is either empty or equal to the container in the request. cnr, isSet := token.EACLTable().CID() if isSet && !cnr.Equals(reqInfo.ContainerID()) { return errBearerInvalidContainerID } // 4. Then check if container owner signed this token. if !bearerSDK.ResolveIssuer(*token).Equals(ownerCnr) { // TODO: #767 in this case we can issue all owner keys from frostfs.id and check once again return errBearerNotSignedByOwner } // 5. Then check if request sender has rights to use this token. var keySender frostfsecdsa.PublicKey err := keySender.Decode(reqInfo.SenderKey()) if err != nil { return fmt.Errorf("decode sender public key: %w", err) } var usrSender user.ID user.IDFromKey(&usrSender, ecdsa.PublicKey(keySender)) if !token.AssertUser(usrSender) { // TODO: #767 in this case we can issue all owner keys from frostfs.id and check once again return errBearerInvalidOwner } return nil }