Getting Logged in User on Client/Designer

I have an RPC endpoint I'd like to protect by checking security levels/zones/roles etc. I wanted to check what the recommended way of handling this would be?

I'm thinking I'll use the GatewayConnectionManager and ClientUserUtilities to pass the entire User object, but is there an easier way to do this based on a session ID or something on the gateway when I receive an RPC request?

Also I haven't done much research on this, but what's the recommended way to check authorization with the new "Security Levels" that you guys have implemented?

You get a ClientReqSession in your RPC handler invocation:

From the session you can retrieve the user object:
ClientReqSession.User clientUser = (ClientReqSession.User) session.getAttribute(ClientReqSession.SESSION_USER);

That ClientUser inner class has entrypoints to the info you care about:
https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.32/com/inductiveautomation/ignition/gateway/clientcomm/ClientReqSession.User.html

To check authorization, you would build or retrieve a PermissionsConfig and then check its isAuthorized method against your user's security levels.

If you create a class instance of your RPC implementation to carry this Session, you probably will want to use a cache of those instances instead of creating one on every RPC call. But, be careful with lifetimes. Both the cache and the implementation instance need to hold the Session with a weak reference or you will prevent the session from being garbage collected after the client/designer closes.

I'm assuming I'd be safe to use a ThreadLocal to hold my session variable here? This is still a thread-per-request because it's a servlet underneath?

Now my next question, if a user is calling a script in Perspective is there a similar mechanism I can use to get the session/user?

That's true, and a good callout.

You could also look it up from the calling thread, e.g.

        if (ClientReqSession.exists()) {
            ClientReqSession session = ClientReqSession.get();

You might also have good luck looking at com.inductiveautomation.ignition.common.tags.model.SecurityContext#THREAD_LOCAL.

If you're already depending on Perspective, there's com.inductiveautomation.perspective.gateway.session.InternalSession#SESSION, another ThreadLocal.

2 Likes

Is there a way to get an AuthenticatedUser from a WebAuthUser? I guess I'm looking for a way to get Roles/Levels/Zones from a WebAuthUser in the end. Am I guaranteed to have a SecurityContext thread local whenever I have a WebAuthUser thread local set?

How are you currently obtaining a WebAuthUser? It looks like there's a sibling class com.inductiveautomation.ignition.common.auth.web.WebAuthUserContext that has zone information. As I understand things, WebAuthUser is the 'base' class for an IdP login, where AuthenticatedUser is the 'base' class for "classic" login using user sources, so I don't think the two are interchangeable in the way you're describing.

1 Like

I was either going to pull it from:

com.inductiveautomation.perspective.gateway.session.InternalSession#SESSION

or by looking up a session-id in the REST API for my app.

I guess let me back up - I'm really looking for a way to build a common/custom SecurityContext object that I can use that will have Zones/Roles/Levels, the User object, and any information required for the audit log (hostname, designer/client/perspective enum etc.). But I'd like to try to build one common object for both RPC and Perspective if possible...

You'll have to create your own facade, with implementations for both a WebAuthUserContext (IdP auth strategy) and AuthenticatedUser (classic user source auth strategy).

We had to do that with Vision Client and Designer sessions when we introduced IdP support for those systems in 8.1. The facade we introduced is interface com.inductiveautomation.ignition.gateway.clientcomm.ClientReqSession.User:

    public interface User {

        default String getUserName() {
            return getUser()
                .map(AuthenticatedUser::getUsername)
                .orElse("unauthenticated");
        }

        ImmutableCollection<String> getRoles();

        ImmutableCollection<String> getSecurityZones();

        ImmutableCollection<SecurityLevelConfig> getSecurityLevels();

        Optional<AuthenticatedUser> getUser();

        QualifiedPath getPath();

    }

We then have a ClassicUser implementation for classic auth which wraps an AuthenticatedUser:

    public static class ClassicUser implements ClientReqSession.User {

        private final AuthenticatedUser authenticatedUser;

        public ClassicUser(AuthenticatedUser authenticatedUser) {
            this.authenticatedUser = authenticatedUser;
        }

        @Override
        public ImmutableCollection<String> getRoles() {
            return ImmutableList.copyOf(authenticatedUser.getRoles());
        }

        @Override
        public ImmutableCollection<String> getSecurityZones() {
            List<?> securityZones = authenticatedUser.get(AuthenticatedUser.SecurityZones);
            return securityZones == null ? ImmutableList.of() : securityZones.stream()
                .filter(String.class::isInstance)
                .map(String.class::cast)
                .collect(ImmutableList.toImmutableList());
        }

        @Override
        public ImmutableCollection<SecurityLevelConfig> getSecurityLevels() {
            ImmutableCollection<String> roles = getRoles();
            ImmutableCollection<String> securityZones = getSecurityZones();
            SecurityLevelConfig rolesSecurityLevel =
                ReservedSecurityLevel.Roles.withChildren(roles.toArray(new String[0]));
            SecurityLevelConfig authenticatedSecurityLevel =
                ReservedSecurityLevel.Authenticated.withChildren(rolesSecurityLevel);
            SecurityLevelConfig securityZonesSecurityLevel =
                ReservedSecurityLevel.SecurityZones.withChildren(securityZones.toArray(new String[0]));
            return ImmutableSet.of(authenticatedSecurityLevel, securityZonesSecurityLevel);
        }

        @Override
        public Optional<AuthenticatedUser> getUser() {
            return Optional.of(authenticatedUser);
        }

        @Override
        public QualifiedPath getPath() {
            return authenticatedUser.getPath();
        }

    }

We also have an IdpUser implementation for IdP-based auth which wraps a WebAuthUserContext:

    private static class IdpUser implements ClientReqSession.User {

        private final WebAuthUserContext webAuthUserContext;
        private final boolean cancelled;

        private IdpUser(WebAuthUserContext webAuthUserContext, boolean cancelled) {
            this.webAuthUserContext = webAuthUserContext;
            this.cancelled = cancelled;
        }

        @Override
        public ImmutableCollection<String> getRoles() {
            return webAuthUserContext.getWebAuthUser()
                .map(WebAuthUser::getRoles)
                .orElseGet(ImmutableSet::of);
        }

        @Override
        public ImmutableCollection<String> getSecurityZones() {
            return webAuthUserContext.getSecurityZones();
        }

        @Override
        public ImmutableCollection<SecurityLevelConfig> getSecurityLevels() {
            return webAuthUserContext.getSecurityLevels();
        }

        @Override
        public Optional<AuthenticatedUser> getUser() {
            return webAuthUserContext.getWebAuthUser().map(user -> {
                BasicAuthenticatedUser authenticatedUser = new BasicAuthenticatedUser(
                    webAuthUserContext.getIdp().orElse(null),
                    user.getId(),
                    user.getUserName(),
                    user.getRoles()
                );
                user.getFirstName()
                    .ifPresent(firstName -> authenticatedUser.set(User.FirstName, firstName));
                user.getLastName()
                    .ifPresent(lastName -> authenticatedUser.set(User.LastName, lastName));
                user.getEmail().ifPresent(email -> authenticatedUser.setContactInfo(List.of(new ContactInfo(
                    ContactType.EMAIL.getContactType(),
                    email
                ))));
                authenticatedUser.set(AuthenticatedUser.SecurityZones, new ArrayList<>(getSecurityZones()));
                return authenticatedUser;
            });
        }

        private QualifiedPath doGetPath() {
            return QualifiedPath.of(
                WellKnownPathTypes.UserProfile, "unknown",
                WellKnownPathTypes.User, "unauthenticated"
            );
        }

        @Override
        public QualifiedPath getPath() {
            return getUser()
                .map(AuthenticatedUser::getPath)
                .orElseGet(this::doGetPath);
        }

    }

Vision Client and Designer sessions were originally designed for Classic auth so the facade interface getUser() method returns an AuthenticatedUser, which is a straight delegation to the underlying AuthenticatedUser that a ClassicUser wraps and IdpUser returns an AuthenticatedUser instance adapted from the WebAuthUserContext instance.

There is a similar adaptation for ClassicUser#getSecurityLevels where we have to build the security levels from the roles and zones set on the AuthenticatedUser, where as an IdpUser delegates to the WebAuthUserContext#getSecurityLevels method since IdP auth strategy speaks security levels natively.

Note: getUser() returns an Optional of AuthenticatedUser because in the IdP auth strategy, it's possible that the user might be authorized to access a project without logging in. For example: if the project is Public or only protected with security zones, the user may not need to log in to access the project...

Hope that helps!

3 Likes

That helps a ton - thank you Joel.

Last (hopefully) question - is there any way I can detect if a script is being called from the gateway scope (tag change script etc. vs perspective session) so that I can inject a system security context? Where do you guys generally use a "system context" inside of Ignition? Just so I can keep my code consistent...

Assuming you are referring to the tag system's SecurityContext (com.inductiveautomation.ignition.common.tags.model.SecurityContext) - We typically assign system context to privileged automated things configured by privileged admins / designers. Think SFCs, Transaction Groups, Gateway timer scripts, etc. They are non-human things that are configured to perform some kind of tag operations. They are not associated with a human via some session (such as Gateway Web Interface, Vision Clients, Perspective Sessions, etc).

Is there any way I can detect if a script is being called from the gateway scope (tag change script etc. vs perspective session) so that I can inject a system security context?

I'm not sure I understand the use case... you're dealing with an RPC function, right? So these are limited to Vision Client Sessions and Designer Sessions. You have everything you need from ClientReqSession that is passed to you. A script shouldn't be calling into your RPC function...

Edit: to clarify, your RPC handler is Gateway-scoped - you provide an instance of it by overriding GatewayModuleHook#getRPCHandler. So you are running in the context of the Gateway. The Platform will provide the ClientReqSession to you, which is essentially a Vision Client or Designer session (you can check which one by calling ClientReqSession#isDesigner).

You can get an instance of the ClientReqSession.User object that I mentioned above which is associated with the given session by calling:

ClientReqSession.User clientUser = (ClientReqSession.User) clientSession.getAttribute(ClientReqSession.SESSION_USER);