Skip to content

Commit

Permalink
Add authType=passthrough support
Browse files Browse the repository at this point in the history
Fixes #7 (Github Issue).

Somewhat related to #11.
  • Loading branch information
spantaleev committed Oct 1, 2020
1 parent 68bf248 commit 476d403
Show file tree
Hide file tree
Showing 15 changed files with 307 additions and 70 deletions.
2 changes: 1 addition & 1 deletion corporal/connector/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type MatrixConnector interface {

DetermineCurrentState(ctx *AccessTokenContext, managedUserIds []string, adminUserId string) (*CurrentState, error)

EnsureUserAccountExists(userId string) error
EnsureUserAccountExists(userId, password string) error

GetUserProfileByUserId(ctx *AccessTokenContext, userId string) (*matrix.ApiUserProfileResponse, error)
SetUserDisplayName(ctx *AccessTokenContext, userId string, displayName string) error
Expand Down
18 changes: 1 addition & 17 deletions corporal/connector/synapse.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (me *SynapseConnector) DetermineCurrentState(
return connectorState, nil
}

func (me *SynapseConnector) EnsureUserAccountExists(userId string) error {
func (me *SynapseConnector) EnsureUserAccountExists(userId, password string) error {
userIdLocalPart, err := gomatrix.ExtractUserLocalpart(userId)
if err != nil {
return err
Expand All @@ -124,22 +124,6 @@ func (me *SynapseConnector) EnsureUserAccountExists(userId string) error {
return err
}

// We create users with random passwords.
// Those passwords are never meant to be given out or used.
//
// Whenever we need to authenticate, we can just obtain an access token
// thanks to shared-secret-auth, regardless of the database password.
// (see ObtainNewAccessTokenForUserId)
//
// Whenever users need to log in, we intercept the /login API
// and possibly turn the call into a request that shared-secret-auth understands
// (see LoginInterceptor).
passwordBytes, err := util.GenerateRandomBytes(64)
if err != nil {
return err
}
password := fmt.Sprintf("%x", passwordBytes)

// Generating the HMAC the same way that the `register_new_matrix_user` script from Matrix Synapse does it.
mac := hmac.New(sha1.New, []byte(me.registrationSharedSecret))
mac.Write([]byte(nonceResponse.Nonce))
Expand Down
19 changes: 16 additions & 3 deletions corporal/httpgateway/interceptor_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@ func (me *LoginInterceptor) Intercept(r *http.Request) InterceptorResponse {
return createInterceptorErrorResponse(loggingContextFields, matrix.ErrorForbidden, "Rejecting non-own domains")
}

policy := me.policyStore.Get()
if policy == nil {
policyObj := me.policyStore.Get()
if policyObj == nil {
return createInterceptorErrorResponse(loggingContextFields, matrix.ErrorUnknown, "Missing policy")
}

userPolicy := policy.GetUserPolicyByUserId(userIdFull)
userPolicy := policyObj.GetUserPolicyByUserId(userIdFull)
if userPolicy == nil {
// Not a user we manage.
// Let it go through and let the upstream server's policies apply, whatever they may be.
Expand All @@ -127,6 +127,19 @@ func (me *LoginInterceptor) Intercept(r *http.Request) InterceptorResponse {
return createInterceptorErrorResponse(loggingContextFields, matrix.ErrorUserDeactivated, "Deactivated in policy")
}

if userPolicy.AuthType == policy.UserAuthTypePassthrough {
// UserAuthTypePassthrough is a special AuthType, authentication for which is not meant to be handled by us.
// Users are created with an initial password as defined in userPolicy.AuthCredential,
// but password-management is then potentially left to the homeserver (depending on policyObj.Flags.AllowCustomPassthroughUserPasswords).
// Authentication always happens at the homeserver.
return InterceptorResponse{
Result: InterceptorResultProxy,
LoggingContextFields: loggingContextFields,
}
}

// Authentication for all other auth types is handled by us (below)

loggingContextFields["authType"] = userPolicy.AuthType

isAuthenticated, err := me.userAuthChecker.Check(
Expand Down
20 changes: 17 additions & 3 deletions corporal/httpgateway/policycheck/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ func CheckUserDeactivate(r *http.Request, ctx context.Context, policy policy.Pol
}

// CheckUserSetPassword is a policy checker for: /_matrix/client/r0/account/password
func CheckUserSetPassword(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
func CheckUserSetPassword(r *http.Request, ctx context.Context, policyObj policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)

userPolicy := policy.GetUserPolicyByUserId(userId)
userPolicy := policyObj.GetUserPolicyByUserId(userId)
if userPolicy == nil {
// Not a user we manage.
// Let it go through and let the upstream server's policies apply, whatever they may be.
Expand All @@ -40,9 +40,23 @@ func CheckUserSetPassword(r *http.Request, ctx context.Context, policy policy.Po
}
}

if userPolicy.AuthType == policy.UserAuthTypePassthrough {
if policyObj.Flags.AllowCustomPassthroughUserPasswords {
return PolicyCheckResponse{
Allow: true,
}
}

return PolicyCheckResponse{
Allow: false,
ErrorCode: matrix.ErrorForbidden,
ErrorMessage: "Denied: passthrough user, but policy does not allow changes",
}
}

return PolicyCheckResponse{
Allow: false,
ErrorCode: matrix.ErrorForbidden,
ErrorMessage: "Denied",
ErrorMessage: "Denied: non-passthrough users are always authenticated against matrix-corporal",
}
}
33 changes: 29 additions & 4 deletions corporal/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,49 @@ func (me *Policy) GetUserPolicyByUserId(userId string) *UserPolicy {
}

type PolicyFlags struct {
// Tells whether users are allowed to have display names,
// AllowCustomUserDisplayNames tells whether users are allowed to have display names,
// which deviate from the ones in the policy.
AllowCustomUserDisplayNames bool `json:"allowCustomUserDisplayNames"`

// Tells whether users are allowed to have avatars,
// AllowCustomUserAvatars tells whether users are allowed to have avatars,
// which deviate from the ones in the policy.
AllowCustomUserAvatars bool `json:"allowCustomUserAvatars"`

// Tells whether users are forbidden from creating rooms.
// AllowCustomPassthroughUserPasswords tells if managed users of AuthType=UserAuthTypePassthrough can change their password.
// This is possible, because their password is stored and managed on the actual homeserver.
// We can let password-changing requests go through.
//
// Users with another AuthType cannot change their password, because authentication happens on our side,
// against the AuthCredential specified in the user's policy.
AllowCustomPassthroughUserPasswords bool `json:"allowCustomPassthroughUserPasswords"`

// ForbidRoomCreation tells whether users are forbidden from creating rooms.
// When there's a dedicated `UserPolicy` for the user, that one takes precedence over this default.
ForbidRoomCreation bool `json:"forbidRoomCreation"`
}

const (
UserAuthTypePassthrough = "passthrough"
UserAuthTypeMd5 = "md5"
UserAuthTypeSha1 = "sha1"
UserAuthTypeSha256 = "sha256"
UserAuthTypeSha512 = "sha512"
)

type UserPolicy struct {
Id string `json:"id"`
Active bool `json:"active"`

AuthType string `json:"authType"`
// AuthType's value is supposed to be one the `UserAuthType*` constants
AuthType string `json:"authType"`

// AuthCredential holds the up-to-date password for all auth types (other than UserAuthTypePassthrough).
//
// If AuthType is NOT UserAuthTypePassthrough, we intercept login requests and authenticate users against this value.
//
// If AuthType is UserAuthTypePassthrough, AuthCredential only serves as the initial password for the user account.
// In such cases, authentication is performed by the homeserver, not by us.
// Subsequent changes to AuthCredential (after the user account has been created) are not reflected.
AuthCredential string `json:"authCredential"`

DisplayName string `json:"displayName"`
Expand Down
32 changes: 31 additions & 1 deletion corporal/reconciliation/computator/computator.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"devture-matrix-corporal/corporal/policy"
"devture-matrix-corporal/corporal/reconciliation"
"devture-matrix-corporal/corporal/util"
"fmt"

"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -95,7 +96,8 @@ func (me *ReconciliationStateComputator) computeUserActivationChanges(
actions = append(actions, &reconciliation.StateAction{
Type: reconciliation.ActionUserCreate,
Payload: map[string]interface{}{
"userId": userPolicy.Id,
"userId": userPolicy.Id,
"password": me.generateInitialPasswordForUser(*userPolicy),
},
})
}
Expand Down Expand Up @@ -378,3 +380,31 @@ func (me *ReconciliationStateComputator) computeUserRoomChanges(

return actions
}

func (me *ReconciliationStateComputator) generateInitialPasswordForUser(userPolicy policy.UserPolicy) string {
// UserAuthTypePassthrough is a special AuthType. Users are created with an initial password as specified in the policy.
// For such users, authentication is delegated to the homeserver.
// We can do password matching on our side as well (at least initially), but delegating authentication to the homeserver,
// allows users to change their password there, etc.
// The actual password on the homeserver may change over time.
if userPolicy.AuthType == policy.UserAuthTypePassthrough {
return userPolicy.AuthCredential
}

// Some other auth type. We create such users with a random password.
// These passwords are never meant to be given out or used.
//
// Whenever we need to authenticate, we can just obtain an access token
// thanks to shared-secret-auth, regardless of the actual password that the user has been created with on the homeserver.
// (see ObtainNewAccessTokenForUserId)
//
// Whenever users need to log in, we intercept the /login API
// and possibly turn the call into a request that shared-secret-auth understands
// (see LoginInterceptor).

passwordBytes, err := util.GenerateRandomBytes(64)
if err != nil {
panic(err)
}
return fmt.Sprintf("%x", passwordBytes)
}
27 changes: 27 additions & 0 deletions corporal/reconciliation/computator/computator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,32 @@ func determineUserActionMismatchError(expected, computed *reconciliation.StateAc
)
}

if expected.Type == reconciliation.ActionUserCreate {
passwordComputed, err := computed.GetStringPayloadDataByKey("password")
if err != nil {
return fmt.Errorf("Did not expect computed %s action to not have a password payload: %s", reconciliation.ActionUserCreate, err)
}

passwordExpected, err := expected.GetStringPayloadDataByKey("password")
if err != nil {
return fmt.Errorf("Did not expect expected %s action to not have a password payload: %s", reconciliation.ActionUserCreate, err)
}

if passwordExpected == "__RANDOM__" {
if len(passwordComputed) != 128 {
return fmt.Errorf("Expected a randomly-generated 128-character password, got: %s", passwordComputed)
}
return nil
}

if passwordExpected != passwordComputed {
return fmt.Errorf("Expected password %s, got %s", passwordExpected, passwordComputed)
}

return nil
}

// TODO - we can validate other actions in more detail

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,19 @@
"users": [
{
"id": "@a:host",
"authType": "plain",
"authCredential": "test",
"active": true,
"joinedCommunityIds": ["+a:host"],
"joinedRoomIds": ["!a:host"]
},
{
"id": "@b:host",
"authType": "sha1",
"authCredential": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
"active": true,
"joinedCommunityIds": [],
"joinedRoomIds": []
}
]
},
Expand All @@ -35,24 +45,31 @@
{
"type": "user.create",
"payload": {
"userId": "@a:host"
"userId": "@a:host",
"password": "__RANDOM__"
}
},

{
"type": "community.join",
"payload": {
"userId": "@a:host",
"communityId": "+a:host"
}
},

{
"type": "room.join",
"payload": {
"userId": "@a:host",
"roomId": "!a:host"
}
},

{
"type": "user.create",
"payload": {
"userId": "@b:host",
"password": "__RANDOM__"
}
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@
{
"type": "user.create",
"payload": {
"userId": "@c:host"
"userId": "@c:host",
"password": "__RANDOM__"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"currentState": {
"users": [
]
},

"policy": {
"schemaVersion": 1,

"flags": {
"allowCustomUserDisplayNames": true,
"allowCustomUserAvatars": true
},

"managedRoomIds": [
"!a:host"
],

"managedCommunityIds": [
"+a:host"
],

"users": [
{
"id": "@a:host",
"authType": "passthrough",
"authCredential": "some-initial-password",
"active": true,
"joinedCommunityIds": ["+a:host"],
"joinedRoomIds": ["!a:host"]
}
]
},

"reconciliationState": {
"actions": [
{
"type": "user.create",
"payload": {
"userId": "@a:host",
"password": "some-initial-password"
}
},

{
"type": "community.join",
"payload": {
"userId": "@a:host",
"communityId": "+a:host"
}
},

{
"type": "room.join",
"payload": {
"userId": "@a:host",
"roomId": "!a:host"
}
}
]
}
}
7 changes: 6 additions & 1 deletion corporal/reconciliation/reconciler/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ func (me *Reconciler) reconcileForActionUserCreate(ctx *connector.AccessTokenCon
return err
}

err = me.connector.EnsureUserAccountExists(userId)
password, err := action.GetStringPayloadDataByKey("password")
if err != nil {
return err
}

err = me.connector.EnsureUserAccountExists(userId, password)
if err != nil {
return fmt.Errorf("Failed ensuring %s is created: %s", userId, err)
}
Expand Down
Loading

0 comments on commit 476d403

Please sign in to comment.