Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow access for non-registered users #475

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions chain-test/src/unit/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GalaChainResponse,
RangedChainObject,
UserAlias,
UserProfile,
signatures
} from "@gala-chain/api";
import { Context, Contract } from "fabric-contract-api";
Expand Down Expand Up @@ -61,20 +62,29 @@ type GalaChainStub = ChaincodeStub & {
getDeletes(): Record<string, true>;
};

interface CallingUserData {
alias?: UserAlias;
ethAddress?: string;
tonAddress?: string;
roles: string[];
}

interface GalaChainContextConfig {
readonly adminPublicKey?: string;
readonly allowNonRegisteredUsers?: boolean;
}

type TestGalaChainContext = Context & {
readonly stub: GalaChainStub;
readonly logger: GalaLoggerInstance;
set callingUserData(d: { alias?: UserAlias; ethAddress?: string; tonAddress?: string; roles: string[] });
set callingUserData(d: CallingUserData);
get callingUser(): UserAlias;
get callingUserEthAddress(): string;
get callingUserRoles(): string[];
get callingUserTonAddress(): string;
setDryRunOnBehalfOf(d: {
alias: UserAlias;
ethAddress?: string;
tonAddress?: string;
roles: string[];
}): void;
get callingUserRoles(): string[];
get callingUserProfile(): UserProfile;
get config(): GalaChainContextConfig;
setDryRunOnBehalfOf(d: CallingUserData): void;
isDryRun: boolean;
get txUnixTime(): number;
setChaincodeStub(stub: ChaincodeStub): void;
Expand Down
7 changes: 4 additions & 3 deletions chaincode/src/contracts/GalaContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { Contract } from "fabric-contract-api";

import { PublicKeyService } from "../services";
import { GalaChainContext, GalaChainStub } from "../types";
import { GalaChainContext, GalaChainContextConfig, GalaChainStub } from "../types";
import { getObjectHistory, getPlainObjectByKey } from "../utils";
import { getApiMethods } from "./GalaContractApi";
import { EVALUATE, GalaTransaction } from "./GalaTransaction";
Expand All @@ -41,7 +41,8 @@ export abstract class GalaContract extends Contract {
*/
constructor(
name: string,
private readonly version: string
private readonly version: string,
private readonly config: GalaChainContextConfig = {}
) {
super(name);
}
Expand All @@ -51,7 +52,7 @@ export abstract class GalaContract extends Contract {
}

public createContext(): GalaChainContext {
return new GalaChainContext();
return new GalaChainContext(this.config);
}

public async beforeTransaction(ctx: GalaChainContext): Promise<void> {
Expand Down
15 changes: 1 addition & 14 deletions chaincode/src/contracts/PublicKeyContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@
*/
import {
ChainCallDTO,
GalaChainResponse,
GetMyProfileDto,
GetPublicKeyDto,
NotFoundError,
PublicKey,
RegisterEthUserDto,
RegisterTonUserDto,
Expand Down Expand Up @@ -67,18 +65,7 @@ export class PublicKeyContract extends GalaContract {
"Since the profile contains also eth address of the user, this method is supported only for signature based authentication."
})
public async GetMyProfile(ctx: GalaChainContext, dto: GetMyProfileDto): Promise<UserProfile> {
// will throw error for legacy auth if the addr is missing
const address = dto.signing === SigningScheme.TON ? ctx.callingUserTonAddress : ctx.callingUserEthAddress;
const profile = await PublicKeyService.getUserProfile(ctx, address);

if (profile === undefined) {
throw new NotFoundError(`UserProfile not found for ${ctx.callingUser}`, {
signature: dto.signature,
user: ctx.callingUser
});
}

return profile;
return ctx.callingUserProfile;
Comment on lines -70 to +68
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this way we can also return default profile here

}

@Submit({
Expand Down
157 changes: 108 additions & 49 deletions chaincode/src/contracts/authenticate.eth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const ___registered = labeled<UserRegistered>("user registered")((ch) => createR
const notRegistered = labeled<UserRegistered>("user not registered")(() => createUser());

type Expectation = (response: unknown, user: User) => void;

const Success = labeled<Expectation>("Success")((response, user) => {
expect(response).toEqual(
transactionSuccess({
Expand All @@ -72,60 +73,118 @@ const Success = labeled<Expectation>("Success")((response, user) => {
);
});

const SuccessNoCustomAlias = labeled<Expectation>("SuccessNoCustomAlias")((response, user) => {
expect(response).toEqual(
transactionSuccess({
alias: `eth|${user.ethAddress}`,
ethAddress: user.ethAddress,
roles: UserProfile.DEFAULT_ROLES
})
);
});

const SuccessUnknownKey = labeled<Expectation>("SuccessUnknownKey")((response, user) => {
expect(response).toEqual(
transactionSuccess({
alias: expect.stringMatching(/^eth\|[a-fA-F0-9]{40}$/),
ethAddress: expect.stringMatching(/^[a-fA-F0-9]{40}$/),
roles: UserProfile.DEFAULT_ROLES
})
);
});

const Error: (errorKey: string) => Expectation = (errorKey) =>
labeled<Expectation>(errorKey)((response) => {
expect(response).toEqual(expect.objectContaining({ Status: 0, ErrorKey: errorKey }));
});

test.each([
[__valid___, _________, ___registered, Success],
[__valid___, _________, notRegistered, Error("USER_NOT_REGISTERED")],
[__valid___, signerKey, ___registered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")],
[__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")],
[__valid___, signerKey, notRegistered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")],
[__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")],
[__valid___, signerAdd, ___registered, Error("REDUNDANT_SIGNER_ADDRESS")],
[__valid___, _wrongAdd, ___registered, Error("ADDRESS_MISMATCH")],
[__valid___, signerAdd, notRegistered, Error("REDUNDANT_SIGNER_ADDRESS")],
[__valid___, _wrongAdd, notRegistered, Error("ADDRESS_MISMATCH")],
[__validDER, _________, ___registered, Error("MISSING_SIGNER")],
[__validDER, _________, notRegistered, Error("MISSING_SIGNER")],
[__validDER, signerKey, ___registered, Success],
[__validDER, _wrongKey, ___registered, Error("PK_INVALID_SIGNATURE")],
[__validDER, signerKey, notRegistered, Error("USER_NOT_REGISTERED")],
[__validDER, signerAdd, ___registered, Success],
[__validDER, _wrongAdd, ___registered, Error("USER_NOT_REGISTERED")],
[__validDER, signerAdd, notRegistered, Error("USER_NOT_REGISTERED")],
[invalid___, _________, ___registered, Error("USER_NOT_REGISTERED")], // tries to get other user's profile
[invalid___, _________, notRegistered, Error("USER_NOT_REGISTERED")],
[invalid___, signerKey, ___registered, Error("PUBLIC_KEY_MISMATCH")],
[invalid___, signerKey, notRegistered, Error("PUBLIC_KEY_MISMATCH")],
[invalid___, signerAdd, ___registered, Error("ADDRESS_MISMATCH")],
[invalid___, signerAdd, notRegistered, Error("ADDRESS_MISMATCH")]
])(
"(sig: %s, dto: %s, user: %s) => %s",
async (
signatureFn: Signature,
publicKeyFn: PublicKey,
createUserFn: UserRegistered,
expectation: Expectation
) => {
// Given
const chaincode = new TestChaincode([PublicKeyContract]);
const userObj = await createUserFn(chaincode);
chaincode.setCallingUser(userObj.alias);

const dto = new ChainCallDTO();
publicKeyFn(dto, userObj);
const signedDto = signatureFn(dto, userObj.privateKey);

// When
const response = await chaincode.invoke("PublicKeyContract:GetMyProfile", signedDto);

// Then
expectation(response, userObj);
}
);
const testFn = async (
signatureFn: Signature,
publicKeyFn: PublicKey,
createUserFn: UserRegistered,
expectation: Expectation
) => {
// Given
const chaincode = new TestChaincode([PublicKeyContract]);
const userObj = await createUserFn(chaincode);
chaincode.setCallingUser(userObj.alias);

const dto = new ChainCallDTO();
publicKeyFn(dto, userObj);
const signedDto = signatureFn(dto, userObj.privateKey);

// When
const response = await chaincode.invoke("PublicKeyContract:GetMyProfile", signedDto);

// Then
expectation(response, userObj);
};

describe("regular flow", () => {
test.each([
[__valid___, _________, ___registered, Success],
[__valid___, _________, notRegistered, Error("USER_NOT_REGISTERED")],
[__valid___, signerKey, ___registered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")],
[__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")],
[__valid___, signerKey, notRegistered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")],
[__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")],
[__valid___, signerAdd, ___registered, Error("REDUNDANT_SIGNER_ADDRESS")],
[__valid___, _wrongAdd, ___registered, Error("ADDRESS_MISMATCH")],
[__valid___, signerAdd, notRegistered, Error("REDUNDANT_SIGNER_ADDRESS")],
[__valid___, _wrongAdd, notRegistered, Error("ADDRESS_MISMATCH")],
[__validDER, _________, ___registered, Error("MISSING_SIGNER")],
[__validDER, _________, notRegistered, Error("MISSING_SIGNER")],
[__validDER, signerKey, ___registered, Success],
[__validDER, _wrongKey, ___registered, Error("PK_INVALID_SIGNATURE")],
[__validDER, signerKey, notRegistered, Error("USER_NOT_REGISTERED")],
[__validDER, signerAdd, ___registered, Success],
[__validDER, _wrongAdd, ___registered, Error("USER_NOT_REGISTERED")],
[__validDER, signerAdd, notRegistered, Error("USER_NOT_REGISTERED")],
[invalid___, _________, ___registered, Error("USER_NOT_REGISTERED")], // tries to get other user's profile
[invalid___, _________, notRegistered, Error("USER_NOT_REGISTERED")],
[invalid___, signerKey, ___registered, Error("PUBLIC_KEY_MISMATCH")],
[invalid___, signerKey, notRegistered, Error("PUBLIC_KEY_MISMATCH")],
[invalid___, signerAdd, ___registered, Error("ADDRESS_MISMATCH")],
[invalid___, signerAdd, notRegistered, Error("ADDRESS_MISMATCH")]
])("(sig: %s, dto: %s, user: %s) => %s", testFn);
});

describe("allowNonRegisteredUsers enabled", () => {
beforeAll(() => {
process.env.ALLOW_NON_REGISTERED_USERS = "true";
});

afterAll(() => {
delete process.env.ALLOW_NON_REGISTERED_USERS;
});

test.each([
[__valid___, _________, ___registered, Success],
[__valid___, _________, notRegistered, SuccessNoCustomAlias],
[__valid___, signerKey, ___registered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")],
[__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")],
[__valid___, signerKey, notRegistered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")],
[__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")],
[__valid___, signerAdd, ___registered, Error("REDUNDANT_SIGNER_ADDRESS")],
[__valid___, _wrongAdd, ___registered, Error("ADDRESS_MISMATCH")],
[__valid___, signerAdd, notRegistered, Error("REDUNDANT_SIGNER_ADDRESS")],
[__valid___, _wrongAdd, notRegistered, Error("ADDRESS_MISMATCH")],
[__validDER, _________, ___registered, Error("MISSING_SIGNER")],
[__validDER, _________, notRegistered, Error("MISSING_SIGNER")],
[__validDER, signerKey, ___registered, Success],
[__validDER, _wrongKey, ___registered, Error("PK_INVALID_SIGNATURE")],
[__validDER, signerKey, notRegistered, SuccessNoCustomAlias],
[__validDER, signerAdd, ___registered, Success],
[__validDER, _wrongAdd, ___registered, Error("USER_NOT_REGISTERED")],
[__validDER, signerAdd, notRegistered, Error("USER_NOT_REGISTERED")],
[invalid___, _________, ___registered, SuccessUnknownKey], // it's just recovered to a different key
[invalid___, _________, notRegistered, SuccessUnknownKey], // it's just recovered to a different key
[invalid___, signerKey, ___registered, Error("PUBLIC_KEY_MISMATCH")],
[invalid___, signerKey, notRegistered, Error("PUBLIC_KEY_MISMATCH")],
[invalid___, signerAdd, ___registered, Error("ADDRESS_MISMATCH")],
[invalid___, signerAdd, notRegistered, Error("ADDRESS_MISMATCH")]
])("(sig: %s, dto: %s, user: %s) => %s", testFn);
});

interface User {
alias: string;
Expand Down
6 changes: 5 additions & 1 deletion chaincode/src/contracts/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ async function getUserProfile(
const profile = await PublicKeyService.getUserProfile(ctx, address);

if (profile === undefined) {
throw new UserNotRegisteredError(address);
if (ctx.config.allowNonRegisteredUsers) {
return PublicKeyService.getDefaultUserProfile(publicKey, signing);
} else {
throw new UserNotRegisteredError(address);
}
}
Comment on lines 140 to 146
Copy link
Collaborator Author

@dzikowski dzikowski Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the location where we make use of the feature flag


return profile;
Expand Down
11 changes: 11 additions & 0 deletions chaincode/src/services/PublicKeyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
UnauthorizedError,
UserAlias,
UserProfile,
asValidUserAlias,
normalizePublicKey,
signatures
} from "@gala-chain/api";
Expand Down Expand Up @@ -142,6 +143,16 @@ export class PublicKeyService {
return undefined;
}

public static getDefaultUserProfile(publicKey: string, signing: SigningScheme): UserProfile {
const address = this.getUserAddress(publicKey, signing);
const profile = new UserProfile();
profile.alias = asValidUserAlias(`${signing.toLowerCase()}|${address}`);
profile.ethAddress = signing === SigningScheme.ETH ? address : undefined;
profile.tonAddress = signing === SigningScheme.TON ? address : undefined;
profile.roles = Array.from(UserProfile.DEFAULT_ROLES);
return profile;
}

public static async getPublicKey(ctx: Context, userId: string): Promise<PublicKey | undefined> {
const key = PublicKeyService.getPublicKeyKey(ctx, userId);
const data = await ctx.stub.getState(key);
Expand Down
37 changes: 35 additions & 2 deletions chaincode/src/types/GalaChainContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { UnauthorizedError, UserAlias, UserRole } from "@gala-chain/api";
import { UnauthorizedError, UserAlias, UserProfile, UserRole } from "@gala-chain/api";
import { Context } from "fabric-contract-api";
import { ChaincodeStub, Timestamp } from "fabric-shim";

Expand All @@ -26,16 +26,40 @@ function getTxUnixTime(ctx: Context): number {
return Math.floor(txUnixTime);
}

export interface GalaChainContextConfig {
readonly adminPublicKey?: string;
readonly allowNonRegisteredUsers?: boolean;
}

class GalaChainContextConfigImpl implements GalaChainContextConfig {
constructor(private readonly config: GalaChainContextConfig) {}

get adminPublicKey(): string | undefined {
return this.config.adminPublicKey ?? process.env.DEV_ADMIN_PUBLIC_KEY;
}

get allowNonRegisteredUsers(): boolean | undefined {
return this.config.allowNonRegisteredUsers ?? process.env.ALLOW_NON_REGISTERED_USERS === "true";
}
}

export class GalaChainContext extends Context {
stub: GalaChainStub;
private callingUserValue?: UserAlias;
private callingUserEthAddressValue?: string;
private callingUserTonAddressValue?: string;
private callingUserRolesValue?: string[];
public isDryRun = false;
private txUnixTimeValue?: number;
private loggerInstance?: GalaLoggerInstance;

public isDryRun = false;
public config: GalaChainContextConfig;

constructor(config: GalaChainContextConfig) {
super();
this.config = new GalaChainContextConfigImpl(config);
}

get logger(): GalaLoggerInstance {
if (this.loggerInstance === undefined) {
this.loggerInstance = new GalaLoggerInstanceImpl(this);
Expand Down Expand Up @@ -76,6 +100,15 @@ export class GalaChainContext extends Context {
return this.callingUserRolesValue;
}

get callingUserProfile(): UserProfile {
const profile = new UserProfile();
profile.alias = this.callingUser;
profile.ethAddress = this.callingUserEthAddressValue;
profile.tonAddress = this.callingUserTonAddressValue;
profile.roles = this.callingUserRoles;
return profile;
}

set callingUserData(d: { alias?: UserAlias; ethAddress?: string; tonAddress?: string; roles: string[] }) {
if (this.callingUserValue !== undefined) {
throw new Error("Calling user already set to " + this.callingUserValue);
Expand Down
Loading
Loading